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
+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