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
+32 -4
View File
@@ -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<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();
+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))
{
+10 -24
View File
@@ -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<int>(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<int>(viewNames.size())))
{
ImGui::ColorEdit4("ClearColor",
glm::value_ptr(ctx.scene->GetSettings().ClearColor));
renderer.SetDebugView(view);
}
}