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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 已等:上一轮该槽位的查询已就绪
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user