diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 7eeffdb..da74d09 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -108,9 +108,8 @@ void Editor::DoStop() { if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return; - std::string envPath; if (!Serialization::SceneSerializer::DeserializeFromString( - *m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot, envPath)) + *m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot)) { ENGINE_ERROR("PIE: snapshot restore failed (scene is now the " "played-out state)"); @@ -445,16 +444,8 @@ void Editor::DoSaveScene() std::filesystem::create_directories( std::filesystem::path(path).parent_path(), ec); - // 环境路径:引擎序列化器只认字符串,查表由编辑器侧负责 - std::string envPath; - if (m_Ctx.getEnvIndex) - { - const int idx = m_Ctx.getEnvIndex(); - if (idx >= 0 && idx < kEnvCount) envPath = kEnvPaths[idx]; - } - if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets, - path, envPath)) + path)) { ENGINE_INFO("Editor: scene saved to '{}'", path); } @@ -472,10 +463,8 @@ void Editor::DoLoadScene() return; } - std::string envPath; if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene, - *m_Ctx.assets, path, - envPath)) + *m_Ctx.assets, path)) { ENGINE_ERROR("Editor: failed to load scene '{}'", path); return; @@ -484,29 +473,4 @@ void Editor::DoLoadScene() // 旧实体已销毁:清选中(连带清描边,见 SelectEntity) SelectEntity(entt::null); - - // 环境恢复:场景带环境且命中环境表才切换(镜像 StatsPanel 同步路径) - if (!envPath.empty() && m_Ctx.getEnvIndex && m_Ctx.setEnvIndex) - { - int found = -1; - for (int i = 0; i < kEnvCount; ++i) - { - if (envPath == kEnvPaths[i]) { found = i; break; } - } - if (found >= 0) - { - m_Ctx.setEnvIndex(found); - const auto env = m_Ctx.assets->LoadEnvironment(kEnvPaths[found]); - if (env.Valid()) - { - m_Ctx.renderer->SetEnvironment( - m_Ctx.assets->GetEnvironment(env)); - } - } - else - { - ENGINE_WARN("Editor: scene env '{}' not in env table, " - "keeping current", envPath); - } - } } diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h index e0cf07c..95a618a 100644 --- a/App/editor/EditorContext.h +++ b/App/editor/EditorContext.h @@ -15,22 +15,6 @@ #include #include // kDisplayViewport / kDisplayDirect -#include - -// 可切换环境(IBL 动态切换):StatsPanel 的 Combo 与 SandboxApp 的 -// 加载逻辑共用这一张表(下标即身份)。 -inline constexpr const char *kEnvPaths[] = { - "assets/textures/venice_dawn_1_4k.hdr", - "assets/textures/rooitou_park_4k.hdr", - "assets/textures/pink_sunrise_4k.hdr", - "assets/textures/birchwood_4k.hdr", -}; -inline constexpr const char *kEnvNames[] = { - "Venice Dawn", "Rooitou Park", "Pink Sunrise", "Birchwood", -}; -inline constexpr int kEnvCount = 4; -inline constexpr int kDefaultEnv = 0; - // 沙盒特有状态(面板可选读取,空指针 = 该面板区块不显示) struct EditorContext { // 引擎门面 @@ -45,9 +29,6 @@ struct EditorContext { // PIE 运行态(Play-In-Editor):Editor 持有,只读指针。 // 面板门控用——运行态禁 Gizmo/编辑器热键,渲染源已是 primary 相机 const bool *playState = nullptr; - - std::function getEnvIndex; // 当前环境表下标 - std::function setEnvIndex; }; #endif //HELLOVULKAN_EDITORCONTEXT_H diff --git a/App/editor/HierarchyPanel.cpp b/App/editor/HierarchyPanel.cpp index 88dec7d..2fdbef3 100644 --- a/App/editor/HierarchyPanel.cpp +++ b/App/editor/HierarchyPanel.cpp @@ -148,11 +148,39 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) { ImGui::Begin("Outliner"); - // 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构) - if (ImGui::Button("+ Create Entity")) + // 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构)。 + // Place Actors 简化语义:常用类型一键带组件,其余 Add Component 补 + if (ImGui::Button("+ Create")) { - const auto e = ctx.scene->CreateEntity("Entity"); - selected = e; + ImGui::OpenPopup("create_entity"); + } + if (ImGui::BeginPopup("create_entity")) + { + if (ImGui::MenuItem("Empty")) + { + selected = ctx.scene->CreateEntity("Entity"); + } + if (ImGui::MenuItem("Light")) + { + auto e = ctx.scene->CreateEntity("Light"); + e.AddComponent(); + selected = e; + } + if (ImGui::MenuItem("Camera")) + { + auto e = ctx.scene->CreateEntity("Camera"); + e.AddComponent(); + selected = e; + } + if (ImGui::MenuItem("Environment")) + { + // UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体 + auto e = ctx.scene->CreateEntity("Environment"); + e.AddComponent(); + e.AddComponent(); + selected = e; + } + ImGui::EndPopup(); } ImGui::Separator(); diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index 74e9a48..f9bb54e 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -58,6 +58,39 @@ namespace return s_Files; } + // 递归收集 assets/ 下环境贴图(.hdr,Skybox 资产槽用;会话级缓存同上) + const std::vector& AssetEnvFiles() + { + static std::vector s_Files; + static bool s_Cached = false; + if (!s_Cached) + { + s_Cached = true; + namespace fs = std::filesystem; + if (fs::exists("assets")) + { + for (const auto& entry : + fs::recursive_directory_iterator("assets")) + { + if (!entry.is_regular_file()) + { + continue; + } + std::string ext = entry.path().extension().string(); + std::transform(ext.begin(), ext.end(), ext.begin(), + [](unsigned char c) + { return std::tolower(c); }); + if (ext == ".hdr") + { + s_Files.push_back(entry.path().generic_string()); + } + } + std::sort(s_Files.begin(), s_Files.end()); + } + } + return s_Files; + } + // InputText 需要 char 缓冲;提交时写回 std::string bool InputTextString(const char* label, std::string& str) { @@ -198,6 +231,9 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) menuItem(static_cast(nullptr), "Mesh Renderer"); menuItem(static_cast(nullptr), "Light"); + menuItem(static_cast(nullptr), "Skybox"); + menuItem(static_cast(nullptr), + "Sky Light"); menuItem(static_cast(nullptr), "Camera"); menuItem(static_cast(nullptr), "Animation"); menuItem(static_cast(nullptr), "Name"); @@ -417,6 +453,78 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) } } + // ---- Skybox(UE Skybox Actor 语义:环境贴图 = 实体资产槽)---- + if (auto* sb = reg.try_get(selected)) + { + bool removeSb = false; + if (ComponentHeader("Skybox", &sb->active, true, &removeSb)) + { + if (!sb->active) + { + ImGui::BeginDisabled(true); + } + // 环境贴图资产槽(模型槽同款:下拉列 assets/ 下 .hdr,选中 + // 即加载;空 = 无环境,只露清屏色)。渲染侧由 Renderer 按 + // 帧数据差量同步(路径变化才重载),此处零额外同步 + { + const auto& s_EnvFiles = AssetEnvFiles(); + std::vector opts; + opts.push_back(""); + for (const auto& f : s_EnvFiles) + { + opts.push_back(f.c_str()); + } + int sel = 0; + for (size_t k = 0; k < s_EnvFiles.size(); ++k) + { + if (s_EnvFiles[k] == sb->envPath) + { + sel = static_cast(k) + 1; + } + } + ImGui::SetNextItemWidth(-1.0f); + if (ImGui::Combo("##env_slot", &sel, opts.data(), + static_cast(opts.size()))) + { + sb->envPath = (sel <= 0) + ? std::string{} + : s_EnvFiles[static_cast(sel) - 1]; + } + ImGui::TextDisabled("(empty = clear color only)"); + } + if (!sb->active) + { + ImGui::EndDisabled(); + } + } + if (removeSb) + { + reg.remove(selected); + } + } + + // ---- SkyLight(UE SkyLight 语义:全局环境光)---- + if (auto* sl = reg.try_get(selected)) + { + bool removeSl = false; + if (ComponentHeader("Sky Light", &sl->active, true, &removeSl)) + { + if (!sl->active) + { + ImGui::BeginDisabled(true); + } + ImGui::ColorEdit4("ambient", &sl->ambientColor.x); + if (!sl->active) + { + ImGui::EndDisabled(); + } + } + if (removeSl) + { + reg.remove(selected); + } + } + // ---- Camera ---- if (auto* cam = reg.try_get(selected)) { diff --git a/App/editor/StatsPanel.cpp b/App/editor/StatsPanel.cpp index 8d6797a..cb2fe35 100644 --- a/App/editor/StatsPanel.cpp +++ b/App/editor/StatsPanel.cpp @@ -120,34 +120,20 @@ void StatsPanel::Draw(const EditorContext& ctx) } } - // ---- Environment:IBL 环境 + 调试视图(决定"看什么")---- + // ---- Environment:清屏色 + 调试视图(决定"看什么")。 + // 环境贴图/环境光已迁为实体组件(Skybox/SkyLight),在 Details 配 ---- if (ImGui::CollapsingHeader("Environment", ImGuiTreeNodeFlags_DefaultOpen)) { - bool& drawSkybox = ctx.scene->GetSettings().DrawSkybox; - ImGui::Checkbox("DrawSkybox", &drawSkybox); - if (drawSkybox && ctx.getEnvIndex) - { - int envIdx = ctx.getEnvIndex(); - if (ImGui::Combo("env", &envIdx, kEnvNames, kEnvCount)) - { - ctx.setEnvIndex(envIdx); - const auto env = ctx.assets->LoadEnvironment(kEnvPaths[envIdx]); - ctx.renderer->SetEnvironment(ctx.assets->GetEnvironment(env)); - } + ImGui::ColorEdit4("ClearColor", + glm::value_ptr(ctx.scene->GetSettings().ClearColor)); - // 调试视图:单看某个 pass 的中间输出(类 UE pass debug) - const auto& viewNames = renderer.GetDebugViewNames(); - int view = renderer.GetDebugView(); - if (ImGui::Combo("Debug View", &view, viewNames.data(), - static_cast(viewNames.size()))) - { - renderer.SetDebugView(view); - } - } - else + // 调试视图:单看某个 pass 的中间输出(类 UE pass debug) + const auto& viewNames = renderer.GetDebugViewNames(); + int view = renderer.GetDebugView(); + if (ImGui::Combo("Debug View", &view, viewNames.data(), + static_cast(viewNames.size()))) { - ImGui::ColorEdit4("ClearColor", - glm::value_ptr(ctx.scene->GetSettings().ClearColor)); + renderer.SetDebugView(view); } } diff --git a/App/main.cpp b/App/main.cpp index d9542dc..19af26a 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -27,7 +27,7 @@ #include #include "Editor.h" -#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用) +#include "EditorContext.h" using namespace Engine; @@ -117,8 +117,6 @@ namespace ctx.assets = &GetAssets(); ctx.app = this; ctx.editorCamera = &GetRenderer().GetEditorCamera(); - ctx.getEnvIndex = [this] { return m_EnvIndex; }; - ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; }; // 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复) Time::SetTimeScale(0.0f); m_Editor->Init(ctx); @@ -135,7 +133,6 @@ namespace private: std::unique_ptr m_Editor = std::make_unique(); - int m_EnvIndex = kDefaultEnv; }; } diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index d375089..2be5255 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -377,6 +377,22 @@ namespace Engine::ECS { bool operator==(const ScriptComponent&) const = default; }; + // 天空盒/环境背景(UE Skybox Actor 语义:环境是场景里的 Actor + + // 组件,不是常驻全局设置)。envPath = 环境贴图资产(.hdr,经 + // AssetManager 加载、Renderer 预积分后进 IBL/Skybox pass);空 = + // 只清屏(ClearColor 露出)。多实例取第一个 active(entt 视图序 = + // 组件创建序,首个命中即稳定) + struct SkyboxComponent { + bool active = true; // 禁用 = 不画天空(清屏色露出) + std::string envPath; + }; + + // 环境光(UE SkyLight 语义:全局漫射环境照明)。多实例取第一个 active + struct SkyLightComponent { + bool active = true; + glm::vec4 ambientColor{0.07f, 0.07f, 0.09f, 0.0f}; // 旧 Settings 默认 + }; + } // namespace Engine::ECS #endif //HELLOVULKAN_COMPONENTS_H diff --git a/Engine/include/Engine/ECS/Scene.h b/Engine/include/Engine/ECS/Scene.h index c77e639..301f22e 100644 --- a/Engine/include/Engine/ECS/Scene.h +++ b/Engine/include/Engine/ECS/Scene.h @@ -40,10 +40,10 @@ namespace Engine::ECS { Scene& operator=(const Scene&) = delete; // ---- 场景级设置 ---- + // 环境类设置已迁为环境实体组件(SkyboxComponent/SkyLightComponent, + // UE Skybox/SkyLight Actor 语义);仅保留渲Target清屏色 struct Settings { glm::vec4 ClearColor = {0.1, 0.1, 0.1, 1.0}; - glm::vec4 AmbientColor{0.07f, 0.07f, 0.09f, 0.0f}; - bool DrawSkybox = true; }; Settings& GetSettings() { return m_Settings; } const Settings& GetSettings() const { return m_Settings; } // 序列化只读 diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 956e8c3..e04fed2 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -211,6 +211,8 @@ namespace Engine { // 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底; // 声明在 m_Passes 之前 → 析构晚于 pass(引用安全) Scope m_Environment; + // 当前绑定环境的资产路径(帧数据 envPath 差量同步基准;空 = 兜底) + std::string m_ActiveEnvPath; Scope m_Swapchain; Scope m_SyncManager; Scope m_CommandManager; diff --git a/Engine/include/Engine/Renderer/SceneTypes.h b/Engine/include/Engine/Renderer/SceneTypes.h index 295289d..cfefefd 100644 --- a/Engine/include/Engine/Renderer/SceneTypes.h +++ b/Engine/include/Engine/Renderer/SceneTypes.h @@ -140,6 +140,10 @@ namespace Engine { glm::vec4 ClearColor{0.1, 0.1, 0.1, 1.0}; bool DrawSkybox = true; + // 本帧生效的环境贴图资产路径(来自第一个 active 的 + // SkyboxComponent;空 = 无环境 → Renderer 回中性兜底 cubemap)。 + // Renderer 据此与当前已绑定环境做差量同步(路径变化才重载) + std::string envPath; // 天空盒反投影矩阵:inverse(projNoJitter × mat4(mat3(view))) //(视图去平移只留旋转 → 反投影结果即世界方向;Skybox pass push) diff --git a/Engine/include/Engine/Serialization/SceneSerializer.h b/Engine/include/Engine/Serialization/SceneSerializer.h index 6c48db5..673b3d2 100644 --- a/Engine/include/Engine/Serialization/SceneSerializer.h +++ b/Engine/include/Engine/Serialization/SceneSerializer.h @@ -8,18 +8,18 @@ // 场景内资产引用双写 {Guid, Path}:GUID 是主键(重命名/移动资产不断 // 引用),Path 是人工可读提示 + 注册表缺失时的解析兜底。 // -// 引擎层身份:本类不感知编辑器(kEnvPaths 表)与渲染器—— -// environmentPath 以普通字符串经参数进出,消费方(编辑器/游戏运行时) -// 自行解释。一套 API 服务两个消费者:编辑器 File 菜单 与 -// 游戏运行时动态加载,不存在"另一套序列化"。 +// 引擎层身份:本类不感知编辑器与渲染器。环境 = 场景实体组件 +//(SkyboxComponent/SkyLightComponent,UE Skybox/SkyLight Actor 语义), +// 随实体序列化,场景级仅存清屏色。 // // 前向兼容(Prefab 路线预留): // - 场景 = 实体节点列表,Prefab 复用同一套实体读写函数 // - Version 闸:高版本文件拒载并 WARN,低版本宽容 // - 未知组件名跳过(Unity 语义:新版场景在旧版引擎打开可玩) // - UUID 重复 → 重新生成(即 Prefab 实例化的 ID 重映射) +// - 旧档 v1 环境语义(文档头 Environment + Settings.AmbientColor/ +// DrawSkybox)→ 自动补建 Environment 实体,视觉等价迁移 // - #ifndef HELLOVULKAN_SCENESERIALIZER_H #define HELLOVULKAN_SCENESERIALIZER_H @@ -37,42 +37,35 @@ namespace Engine::Serialization { class SceneSerializer { public: // ---- 保存 ---- - // scene 序列化到 outPath(YAML)。environmentPath 可空(不写 - // Environment 字段)。失败(无法写文件)返回 false。 + // scene 序列化到 outPath(YAML)。失败(无法写文件)返回 false。 static bool Serialize(const ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& outPath, - const std::string& environmentPath = {}); + const std::string& outPath); // ---- 加载 ---- // 原子换血:先解析校验到中间结构,全部成功才 scene.Clear() 重建 - //(解析失败不动现场)。成功时 outEnvironmentPath 带回场景记录的 - // 环境路径(空 = 场景未带环境,消费方保持当前环境即可)。 + //(解析失败不动现场)。 // 健壮性:单实体损坏跳过继续、缺字段取默认值、未知组件跳过、 // UUID 重复重新生成、YAML 异常优雅报错。 // 注意:调用方持有的 Entity/EntityHandle 在成功后全部悬空, // 必须重解析(Scene::GetEntityHandle / 按 UUID 查询)。 static bool Deserialize(ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& inPath, - std::string& outEnvironmentPath); + const std::string& inPath); // ---- 内存快照(同一套实体读写,不同载体)---- // 消费者:编辑器 Play-In-Editor 快照恢复、Prefab、运行时动态 - // 拼装场景。documentName 仅写入文档头 "Scene" 字段(注释性), - // 快照可传实体名或留空;environmentPath 语义同文件版。 + // 拼装场景。documentName 仅写入文档头 "Scene" 字段(注释性)。 static std::string SerializeToString( const ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& documentName = {}, - const std::string& environmentPath = {}); + const std::string& documentName = {}); // 失败(YAML 异常/版本过高)返回 false 且不动现场。 // 实体悬空语义同文件版 Deserialize。 static bool DeserializeFromString(ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& yamlText, - std::string& outEnvironmentPath); + const std::string& yamlText); }; } // namespace Engine::Serialization diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index a2630ae..ffb000a 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -601,6 +601,31 @@ namespace Engine } const FrameSceneData& frame = m_SceneRenderer->Frame(); + // 1.5 环境差量同步(UE 语义:环境是场景组件数据,渲染器被动跟随)。 + // 帧数据 envPath 与当前已绑定环境做比较,路径变化才重载/重绑 + //(LoadEnvironment 缓存命中零开销;失效路径一次失败即记, + // 不逐帧重试,路径改回时自然恢复) + if (frame.envPath != m_ActiveEnvPath) + { + m_ActiveEnvPath = frame.envPath; + if (m_ActiveEnvPath.empty()) + { + SetEnvironment(nullptr); // 回中性兜底 + } + else + { + const auto ref = assets.LoadEnvironment(m_ActiveEnvPath); + SetEnvironment(ref.Valid() ? assets.GetEnvironment(ref) + : nullptr); + if (!ref.Valid()) + { + ENGINE_WARN("Renderer: env '{}' failed to load, " + "neutral fallback active", + m_ActiveEnvPath); + } + } + } + // 2. 等本槽位 fence → 写 UBO/SSBO → reset(写-读竞争关闭) m_SyncManager->WaitFence(frameSlot); ReadGpuTimes(frameSlot); // fence 已等:上一轮该槽位的查询已就绪 diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 8cc5ade..abeef82 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -312,11 +312,35 @@ namespace Engine ubo.proj[2][1] += jy * 2.0f / static_cast(targetExtent.height); } - ubo.ambientColor = scene.GetSettings().AmbientColor; + // ---- 环境组件落帧(UE SkyLight/Skybox Actor 语义):场景环境 + // 是实体组件而非全局设置。多实例取第一个 active(entt + // 视图序 = 组件创建序,首个命中稳定);无组件场景兜底 = + // 无天空 + 旧 Settings 默认环境光(空场景清屏可渲染)---- + m_Frame.DrawSkybox = false; + m_Frame.envPath.clear(); + ubo.ambientColor = glm::vec4{0.07f, 0.07f, 0.09f, 0.0f}; + for (auto&& [e, sb] : + scene.GetRegistry().view().each()) + { + if (sb.active) + { + m_Frame.DrawSkybox = true; + m_Frame.envPath = sb.envPath; + break; + } + } + for (auto&& [e, sl] : + scene.GetRegistry().view().each()) + { + if (sl.active) + { + ubo.ambientColor = sl.ambientColor; + break; + } + } // ---- 运行时开关/调试视图落帧(各 pass 与 frag 只读契约字段)---- m_Frame.ClearColor = scene.GetSettings().ClearColor; - m_Frame.DrawSkybox = scene.GetSettings().DrawSkybox; m_Frame.shadowsEnabled = m_ShadowsEnabled; m_Frame.taaEnabled = m_TAAEnabled; m_Frame.bloomEnabled = m_BloomEnabled; diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index 0bc62cd..be4f926 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -279,6 +279,21 @@ namespace Engine::Serialization { out << YAML::EndMap; } + if (const auto* sb = reg.try_get(e)) { + out << YAML::Key << "Skybox" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "Active" << YAML::Value << sb->active; + out << YAML::Key << "EnvMap" << YAML::Value << sb->envPath; + out << YAML::EndMap; + } + + if (const auto* sl = reg.try_get(e)) { + out << YAML::Key << "SkyLight" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "Ambient" << YAML::Value; + EmitVec4(out, sl->ambientColor); + out << YAML::Key << "Active" << YAML::Value << sl->active; + out << YAML::EndMap; + } + if (const auto* rb = reg.try_get(e)) { out << YAML::Key << "RigidBody" << YAML::Value << YAML::BeginMap; @@ -397,8 +412,7 @@ namespace Engine::Serialization { // 文档头 "Scene" 字段(快照无文件名,该字段是注释性的) static std::string SerializeDoc(const ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& documentName, - const std::string& environmentPath) + const std::string& documentName) { // float32 无损往返需 9 位有效数字(std::numeric_limits // ::max_digits10) @@ -410,18 +424,11 @@ namespace Engine::Serialization { out << YAML::Key << "Scene" << YAML::Value << documentName; } out << YAML::Key << "Version" << YAML::Value << kSceneVersion; - if (!environmentPath.empty()) { - out << YAML::Key << "Environment" << YAML::Value - << environmentPath; - } const auto& s = scene.GetSettings(); out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap; out << YAML::Key << "ClearColor" << YAML::Value; EmitVec4(out, s.ClearColor); - out << YAML::Key << "AmbientColor" << YAML::Value; - EmitVec4(out, s.AmbientColor); - out << YAML::Key << "DrawSkybox" << YAML::Value << s.DrawSkybox; out << YAML::EndMap; out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq; @@ -442,14 +449,12 @@ namespace Engine::Serialization { bool SceneSerializer::Serialize(const ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& outPath, - const std::string& environmentPath) + const std::string& outPath) { try { const std::string doc = SerializeDoc( scene, assets, - std::filesystem::path(outPath).stem().string(), - environmentPath); + std::filesystem::path(outPath).stem().string()); if (doc.empty()) { return false; // emit 失败,日志已在 SerializeDoc 打过 } @@ -477,11 +482,10 @@ namespace Engine::Serialization { std::string SceneSerializer::SerializeToString( const ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& documentName, - const std::string& environmentPath) + const std::string& documentName) { try { - return SerializeDoc(scene, assets, documentName, environmentPath); + return SerializeDoc(scene, assets, documentName); } catch (const YAML::Exception& e) { ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what()); @@ -500,11 +504,8 @@ namespace Engine::Serialization { static bool ParseRoot(ECS::Scene& scene, Assets::AssetManager& assets, const YAML::Node& root, - const std::string& sourceName, - std::string& outEnvironmentPath) + const std::string& sourceName) { - outEnvironmentPath.clear(); - // Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档 if (const int version = root["Version"].as(0); version > kSceneVersion) { @@ -513,14 +514,27 @@ namespace Engine::Serialization { return false; } - // 僵尸节点安全:as(fallback) 对缺失键返回 fallback - outEnvironmentPath = root["Environment"].as(""); - + // ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)---- + // 僵尸节点安全:as(fallback) 对缺失键返回 fallback;环境 + // 键仅在真实存在时置位(避免新档误触发兼容建实体) + const std::string legacyEnv = + root["Environment"].as(""); + bool legacyEnvKey = !legacyEnv.empty(); + glm::vec4 legacyAmbient{0.07f, 0.07f, 0.09f, 0.0f}; + bool legacyAmbientKey = false; + bool legacyDrawSkybox = true; + bool legacySkyboxKey = false; if (const YAML::Node st = root["Settings"]; st && st.IsMap()) { auto& s = scene.GetSettings(); s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor); - s.AmbientColor = ReadVec4(st["AmbientColor"], s.AmbientColor); - s.DrawSkybox = st["DrawSkybox"].as(s.DrawSkybox); + if (const YAML::Node amb = st["AmbientColor"]) { + legacyAmbient = ReadVec4(amb, legacyAmbient); + legacyAmbientKey = true; + } + if (const YAML::Node ds = st["DrawSkybox"]) { + legacyDrawSkybox = ds.as(legacyDrawSkybox); + legacySkyboxKey = true; + } } // ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)---- @@ -746,6 +760,39 @@ namespace Engine::Serialization { } } } + + if (const YAML::Node n = comps["Skybox"]) { + auto& sb = handle.AddComponent(); + sb.active = n["Active"].as(true); + sb.envPath = n["EnvMap"].as(""); + } + + if (const YAML::Node n = comps["SkyLight"]) { + auto& sl = handle.AddComponent(); + sl.ambientColor = ReadVec4(n["Ambient"], sl.ambientColor); + sl.active = n["Active"].as(true); + } + } + + // ---- 旧档环境兼容迁移:v1 的 Environment/Settings.AmbientColor/ + // DrawSkybox → 补建一个 Environment 实体(视觉等价)。仅当旧键 + // 真实存在且实体列表没有环境组件时触发(新档缺省键不触发; + // 快照恢复同理——快照里组件已实体化,不走这里) + const bool legacyEnvPresent = + legacyEnvKey || legacyAmbientKey || legacySkyboxKey; + const bool hasEnvComponents = + scene.GetRegistry().view().size() > 0 || + scene.GetRegistry().view().size() > 0; + if (legacyEnvPresent && !hasEnvComponents) { + auto env = scene.CreateEntity("Environment"); + auto& sb = env.AddComponent(); + sb.envPath = legacyEnv; + sb.active = legacyDrawSkybox; + auto& sl = env.AddComponent(); + sl.ambientColor = legacyAmbient; + ENGINE_INFO("SceneSerializer: legacy environment migrated to " + "'Environment' entity (env='{}')", + legacyEnv); } // ---- 阶段 3:重链父子(两遍解析:父可能出现在子之后)。 @@ -779,8 +826,7 @@ namespace Engine::Serialization { bool SceneSerializer::Deserialize(ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& inPath, - std::string& outEnvironmentPath) + const std::string& inPath) { YAML::Node root; try { @@ -796,14 +842,13 @@ namespace Engine::Serialization { e.what()); return false; } - return ParseRoot(scene, assets, root, inPath, outEnvironmentPath); + return ParseRoot(scene, assets, root, inPath); } bool SceneSerializer::DeserializeFromString( ECS::Scene& scene, Assets::AssetManager& assets, - const std::string& yamlText, - std::string& outEnvironmentPath) + const std::string& yamlText) { YAML::Node root; try { @@ -819,7 +864,7 @@ namespace Engine::Serialization { e.what()); return false; } - return ParseRoot(scene, assets, root, "snapshot", outEnvironmentPath); + return ParseRoot(scene, assets, root, "snapshot"); } } // namespace Engine::Serialization