Environment settings become scene entities (UE Skybox/SkyLight semantics)

Skybox/SkyLight are now Actor-carried components instead of resident
global settings, matching UE where SkyAtmosphere/SkyLight/fog are
Actors added from Place Actors and configured in Details.

- New SkyboxComponent (active + envPath asset slot) and SkyLightComponent
  (active + ambientColor); first-active entity wins per frame
- SceneRenderer collects env from components into FrameSceneData
  (DrawSkybox/envPath/ambient); Renderer performs delta sync - env
  reloads only when the resolved path changes, neutral fallback when
  empty or unloadable
- App-side env plumbing removed: kEnvPaths table and getEnvIndex/
  setEnvIndex callbacks deleted; StatsPanel keeps ClearColor + debug
  view, env params move to Details
- Serializer: scene-level Settings stores ClearColor only; env
  components serialize per entity; legacy v1 scenes (Environment header
  + Settings.AmbientColor/DrawSkybox) auto-migrate into an Environment
  entity on load for visual parity
- Inspector: Skybox/Sky Light sections with .hdr asset slot dropdown
  and Add Component entries; Outliner "+ Create" popup offers
  Empty/Light/Camera/Environment
This commit is contained in:
2026-10-05 01:02:21 +08:00
parent b7d6f53591
commit 432089747f
14 changed files with 317 additions and 144 deletions
+3 -39
View File
@@ -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);
}
}
}
-19
View File
@@ -15,22 +15,6 @@
#include <Engine/Renderer/EditorCamera.h>
#include <Engine/Renderer/SceneTypes.h> // kDisplayViewport / kDisplayDirect
#include <functional>
// 可切换环境(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<int()> getEnvIndex; // 当前环境表下标
std::function<void(int)> setEnvIndex;
};
#endif //HELLOVULKAN_EDITORCONTEXT_H
+31 -3
View File
@@ -148,12 +148,40 @@ 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");
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<Engine::ECS::LightComponent>();
selected = e;
}
if (ImGui::MenuItem("Camera"))
{
auto e = ctx.scene->CreateEntity("Camera");
e.AddComponent<Engine::ECS::CameraComponent>();
selected = e;
}
if (ImGui::MenuItem("Environment"))
{
// UE 新关卡环境集简化:Skybox(背景)+ SkyLight(环境光)一体
auto e = ctx.scene->CreateEntity("Environment");
e.AddComponent<Engine::ECS::SkyboxComponent>();
e.AddComponent<Engine::ECS::SkyLightComponent>();
selected = e;
}
ImGui::EndPopup();
}
ImGui::Separator();
auto& reg = ctx.scene->GetRegistry();
+108
View File
@@ -58,6 +58,39 @@ namespace
return s_Files;
}
// 递归收集 assets/ 下环境贴图(.hdr,Skybox 资产槽用;会话级缓存同上)
const std::vector<std::string>& AssetEnvFiles()
{
static std::vector<std::string> 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<Engine::ECS::MeshRendererComponent*>(nullptr),
"Mesh Renderer");
menuItem(static_cast<Engine::ECS::LightComponent*>(nullptr), "Light");
menuItem(static_cast<Engine::ECS::SkyboxComponent*>(nullptr), "Skybox");
menuItem(static_cast<Engine::ECS::SkyLightComponent*>(nullptr),
"Sky Light");
menuItem(static_cast<Engine::ECS::CameraComponent*>(nullptr), "Camera");
menuItem(static_cast<Engine::ECS::AnimationComponent*>(nullptr), "Animation");
menuItem(static_cast<Engine::ECS::NameComponent*>(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<Engine::ECS::SkyboxComponent>(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<const char*> opts;
opts.push_back("<none>");
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<int>(k) + 1;
}
}
ImGui::SetNextItemWidth(-1.0f);
if (ImGui::Combo("##env_slot", &sel, opts.data(),
static_cast<int>(opts.size())))
{
sb->envPath = (sel <= 0)
? std::string{}
: s_EnvFiles[static_cast<size_t>(sel) - 1];
}
ImGui::TextDisabled("(empty = clear color only)");
}
if (!sb->active)
{
ImGui::EndDisabled();
}
}
if (removeSb)
{
reg.remove<Engine::ECS::SkyboxComponent>(selected);
}
}
// ---- SkyLight(UE SkyLight 语义:全局环境光)----
if (auto* sl = reg.try_get<Engine::ECS::SkyLightComponent>(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<Engine::ECS::SkyLightComponent>(selected);
}
}
// ---- Camera ----
if (auto* cam = reg.try_get<Engine::ECS::CameraComponent>(selected))
{
+4 -18
View File
@@ -120,20 +120,12 @@ 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();
@@ -144,12 +136,6 @@ void StatsPanel::Draw(const EditorContext& ctx)
renderer.SetDebugView(view);
}
}
else
{
ImGui::ColorEdit4("ClearColor",
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
}
}
// ---- Physics Debug:物理调试可视化(collider/约束/重力/速度/加速度)----
if (ImGui::CollapsingHeader("Physics Debug"))
+1 -4
View File
@@ -27,7 +27,7 @@
#include <GLFW/glfw3.h>
#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<Editor> m_Editor = std::make_unique<Editor>();
int m_EnvIndex = kDefaultEnv;
};
}
+16
View File
@@ -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
+2 -2
View File
@@ -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; } // 序列化只读
@@ -211,6 +211,8 @@ namespace Engine {
// 中性兜底环境(IBL 四件套):Application 未加载/加载失败时保底;
// 声明在 m_Passes 之前 → 析构晚于 pass(引用安全)
Scope<Cubemap> m_Environment;
// 当前绑定环境的资产路径(帧数据 envPath 差量同步基准;空 = 兜底)
std::string m_ActiveEnvPath;
Scope<Swapchain> m_Swapchain;
Scope<SyncManager> m_SyncManager;
Scope<CommandManager> m_CommandManager;
@@ -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)
@@ -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
+25
View File
@@ -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 已等:上一轮该槽位的查询已就绪
+26 -2
View File
@@ -312,11 +312,35 @@ namespace Engine
ubo.proj[2][1] += jy * 2.0f / static_cast<f32>(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<ECS::SkyboxComponent>().each())
{
if (sb.active)
{
m_Frame.DrawSkybox = true;
m_Frame.envPath = sb.envPath;
break;
}
}
for (auto&& [e, sl] :
scene.GetRegistry().view<ECS::SkyLightComponent>().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;
+76 -31
View File
@@ -279,6 +279,21 @@ namespace Engine::Serialization {
out << YAML::EndMap;
}
if (const auto* sb = reg.try_get<ECS::SkyboxComponent>(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<ECS::SkyLightComponent>(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<ECS::RigidBodyComponent>(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
// <float>::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<int>(0);
version > kSceneVersion) {
@@ -513,14 +514,27 @@ namespace Engine::Serialization {
return false;
}
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback
outEnvironmentPath = root["Environment"].as<std::string>("");
// ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)----
// 僵尸节点安全:as<T>(fallback) 对缺失键返回 fallback;环境
// 键仅在真实存在时置位(避免新档误触发兼容建实体)
const std::string legacyEnv =
root["Environment"].as<std::string>("");
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<bool>(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<bool>(legacyDrawSkybox);
legacySkyboxKey = true;
}
}
// ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)----
@@ -746,6 +760,39 @@ namespace Engine::Serialization {
}
}
}
if (const YAML::Node n = comps["Skybox"]) {
auto& sb = handle.AddComponent<ECS::SkyboxComponent>();
sb.active = n["Active"].as<bool>(true);
sb.envPath = n["EnvMap"].as<std::string>("");
}
if (const YAML::Node n = comps["SkyLight"]) {
auto& sl = handle.AddComponent<ECS::SkyLightComponent>();
sl.ambientColor = ReadVec4(n["Ambient"], sl.ambientColor);
sl.active = n["Active"].as<bool>(true);
}
}
// ---- 旧档环境兼容迁移:v1 的 Environment/Settings.AmbientColor/
// DrawSkybox → 补建一个 Environment 实体(视觉等价)。仅当旧键
// 真实存在且实体列表没有环境组件时触发(新档缺省键不触发;
// 快照恢复同理——快照里组件已实体化,不走这里)
const bool legacyEnvPresent =
legacyEnvKey || legacyAmbientKey || legacySkyboxKey;
const bool hasEnvComponents =
scene.GetRegistry().view<ECS::SkyboxComponent>().size() > 0 ||
scene.GetRegistry().view<ECS::SkyLightComponent>().size() > 0;
if (legacyEnvPresent && !hasEnvComponents) {
auto env = scene.CreateEntity("Environment");
auto& sb = env.AddComponent<ECS::SkyboxComponent>();
sb.envPath = legacyEnv;
sb.active = legacyDrawSkybox;
auto& sl = env.AddComponent<ECS::SkyLightComponent>();
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