editor camera: dedicated Scene-view camera decoupled from camera entities

EditorCamera (UE FEditorViewportClient semantics) owned by SceneRenderer:
own TransformComponent + reused Camera controller. BuildFrame dispatches
render source by display mode — editor form always renders from the
editor camera (usable in empty scenes), game form renders from the
scene's primary camera entity. AlignTo copies an entity pose into the
editor camera ("Align Viewport to Camera" button in Inspector). F focus
and viewport gizmo now follow the same dispatch. main.cpp stripped of
dangling sandbox camera/spinner state; onSceneLoaded hook removed.
This commit is contained in:
2026-10-03 15:52:28 +08:00
parent e2a9c21e68
commit fae6614e70
12 changed files with 192 additions and 229 deletions
+14 -190
View File
@@ -1,15 +1,18 @@
//
// Created by atdunbg on 2026/9/20.
//
// SandboxApp:第一个基于 Application 框架的程序。
// SandboxApp:基于 Application 框架的编辑器宿主程序。
//
// main 的职责边界:
// - 定义 Application 派生类并运行它
// - "程序逻辑":场景搭建(实体/组件)、逐帧更新、事件响应、
// 异步环境加载
// - 编辑器形态驱动 EditorCamera(Scene view 专用相机,非场景对象)
// - 异步环境加载、全局热键、输入归属门控
// - UI 全部委托 App/editor/ 的 docking Editor(本文件不再出现
// 任何 ImGui 控件)
//
// 场景内容(实体/组件)由编辑器 File 菜单 Save/Load 场景文件管理,
// 相机实体属于场景数据、只在游戏形态参与渲染(UE 语义)。
//
// 窗口、渲染器、事件泵、帧计时、场景容器全部由框架管理。
//
@@ -18,16 +21,8 @@
#include <Engine/Core/Window.h> // SetInputCapture(Application.h 只前置声明)
#include <Engine/Core/Log.h>
#include <Engine/Core/Stats.h>
#include <Engine/Core/AsyncTask.h>
#include <Engine/ECS/Scene.h>
#include <Engine/ECS/EntityHandle.h>
#include <Engine/ECS/Components.h>
#include <Engine/Assets/AssetManager.h>
#include <Engine/Renderer/Renderer.h>
#include <Engine/Renderer/Model.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <Engine/Renderer/EditorCamera.h>
#include <GLFW/glfw3.h>
@@ -35,12 +30,9 @@
#include "EditorContext.h" // kEnvPaths / kDefaultEnv(与 StatsPanel 共用)
using namespace Engine;
using namespace Engine::ECS;
namespace
{
constexpr const char* kTemplePath = "assets/models/SunTemple/SunTemple.fbx";
class SandboxApp final : public Application
{
public:
@@ -58,7 +50,6 @@ namespace
void OnInit() override
{
Scene& scene = GetScene();
Renderer& renderer = GetRenderer();
renderer.SetDisplayMode(kDisplayViewport);
@@ -68,162 +59,17 @@ namespace
// Editor 注入:引擎门面 + 沙盒能力回调(环境异步切换等)
InitEditor();
// 默认环境(IBL 四件套):经资产层加载(compute 预积分,毫秒级),
// 其余环境在 Stats 面板里按需 lazy 加载后切换
const auto env = GetAssets().LoadEnvironment(kEnvPaths[0]);
if (env.Valid()) {
GetRenderer().SetEnvironment(GetAssets().GetEnvironment(env));
}
/*
const auto suntemple = GetAssets().LoadModel(kTemplePath);
auto entityhandle = scene.CreateEntity("SunTemple");
entityhandle.AddComponent<MeshRendererComponent>(MeshRendererComponent{suntemple});
*/
scene.CreateEntity("test entity");
scene.CreateEntity("test entity");
scene.CreateEntity("test entity");
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
// 物理验证:Dynamic 刚体(默认 Box halfExtents 0.5),
// 出生位置悬空 → 应下落至地面静止(Transform 由物理写回)
m_Gun.AddComponent<RigidBodyComponent>();
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
// 地面:程序化纯色平面(半边长 15,中心 y=-0.5)+ 静态盒刚体
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
auto& groundRb = m_Ground.AddComponent<RigidBodyComponent>();
groundRb.type = RigidBodyComponent::BodyType::Static;
groundRb.boxHalfExtents = glm::vec3(15.0f, 0.1f, 15.0f); // 平面顶点烘焙 ±15,不能用默认 0.5
/*
const auto gun = GetAssets().LoadModel("assets/models/m1911/M1911.fbx");
if (gun.Valid()) {
m_Gun = scene.CreateEntity("M1911");
m_Gun.AddComponent<MeshRendererComponent>(MeshRendererComponent{gun});
m_Gun.AddComponent<AnimationComponent>();
auto& gt = m_Gun.GetComponent<TransformComponent>();
gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f));
} else {
ENGINE_WARN("m1911 model not loaded, animation test skipped");
}
int countSize = 0;
if (countSize) {
// 模型资产(PBR 验证场景:backpack 为旧式贴图资产,
// 退化为电介质因子路径——albedo+normal,metallic=0/roughness=0.5)
const auto model = GetAssets().LoadModel("assets/models/backpack/backpack.obj");
// const auto model = GetAssets().LoadModel("assets/models/pocket_watch_4k.gltf/pocket_watch_4k.gltf");
for (int i = 0; i < countSize; i ++)
{
for (int j = 0; j < countSize; j ++)
{
auto m = scene.CreateEntity();
m.AddComponent<MeshRendererComponent>(MeshRendererComponent{model});
m.GetComponent<TransformComponent>().SetPosition(glm::vec3{(i - countSize * 0.5) * 2, 0, (j - countSize * 0.5) * 2});
m_Spinners.push_back(m);
}
}
}
// 地面:程序化纯色平面(半边长 15,中心 y=-2)
const auto ground = GetAssets().CreatePlane(15.0f, 140, 140, 145);
m_Ground = scene.CreateEntity("Ground");
m_Ground.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose
m_Ground.AddComponent<MeshRendererComponent>(MeshRendererComponent{ground});
*/
// 相机实体(primary = 当前渲染视角;位姿存实体 Transform)
m_Camera = scene.CreateEntity("MainCamera");
auto& camera = m_Camera.AddComponent<CameraComponent>();
camera.primary = true;
m_Camera.GetComponent<TransformComponent>()
.SetPosition(glm::vec3(0.0f, 12.0f, 22.0f));
m_Camera.GetComponent<TransformComponent>()
.SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f));
// 灯光实体(第一个方向光 = 阴影光源)
m_KeyLight = scene.CreateEntity("KeyLight");
m_KeyLight.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Directional,
.direction = glm::vec3(-0.5f, -0.6f, -0.35f),
.color = glm::vec4(1.0f, 0.95f, 0.9f, 5.0f),
.castShadow = true,
});
/*
scene.CreateEntity("FillLight")
.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Directional,
.direction = glm::vec3(0.5f, -0.5f, 0.3f),
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.5f)
});
*/
/*
// 点光源(位置 = 实体 Transform,出生即有;无阴影,距离衰减)
auto fire = scene.CreateEntity("FirePitLight");
fire.GetComponent<TransformComponent>().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f));
fire.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Point,
.color = glm::vec4(1.0f, 0.6f, 0.3f, 1.0f), // 暖橙
.radius = 8.0f,
});
auto cool = scene.CreateEntity("CoolFillLight");
cool.GetComponent<TransformComponent>().SetPosition(glm::vec3(-2.5f, 2.0f, -2.0f));
cool.AddComponent<LightComponent>(
LightComponent{
.type = LightComponent::Type::Point,
.color = glm::vec4(0.3f, 0.5f, 1.0f, 1.2f), // 冷蓝
.radius = 10.0f,
});
*/
}
void OnUpdate(const FrameTime& frameTime) override
{
// 加载过无相机实体的场景(或场景被清空):相机句柄悬空,
// 直接跳过本帧游戏逻辑(渲染仍以编辑器视角进行)
if (!m_Camera.IsAlive()) return;
// 相机(RMB 飞行 / MMB 焦点环绕 / 滚轮推拉;位姿读写实体
// Transform)。编辑器相机走真实轨:暂停(timeScale=0)时仍
// 可飞行观察。编辑器模式移动键需 RMB 按住(UE 语义:飞行时
// 键盘归相机,W/E/R 归 Gizmo)
m_Camera.GetComponent<CameraComponent>()
.camera.Update(GetWindow(),
m_Camera.GetComponent<TransformComponent>(),
frameTime.UnscaledDt(),
GetRenderer().GetDisplayMode() == kDisplayViewport);
// 旋转动画:编辑 Transform 分量后 Compose 重算 world。
// 动画时钟 = Time::Elapsed()(scaled 累计,暂停自动冻结——
// 帧历史验证:暂停后 Temporal Diff 视图 2 帧内整屏变黑)
for (size_t i = 0; i < m_Spinners.size(); ++i)
// 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 /
// 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。
// 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止)
if (GetRenderer().GetDisplayMode() == kDisplayViewport)
{
auto& t = m_Spinners[i].GetComponent<TransformComponent>();
t.SetRotation(glm::vec3(
0.0f, static_cast<f32>(Time::Elapsed()) * 60.0f +
static_cast<f32>(i) * 40.0f, 0.0f));
GetRenderer().GetEditorCamera().Update(GetWindow(),
frameTime.UnscaledDt());
}
}
@@ -270,25 +116,9 @@ namespace
ctx.scene = &GetScene();
ctx.assets = &GetAssets();
ctx.app = this;
ctx.editorCamera = &GetRenderer().GetEditorCamera();
ctx.getEnvIndex = [this] { return m_EnvIndex; };
ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; };
// 场景加载成功 → 沙盒侧悬空句柄重解析(registry 清场重建后
// m_Camera 等成员句柄全部失效,不重解析则下一帧必崩)
ctx.onSceneLoaded = [this] {
Scene& scene = GetScene();
m_Spinners.clear();
m_Gun = {};
m_Ground = {};
m_KeyLight = {};
entt::entity camEnt = entt::null;
scene.Each<CameraComponent>([&](entt::entity e,
CameraComponent& cc) {
if (cc.primary) camEnt = e;
});
m_Camera = scene.GetEntityHandle(camEnt); // 无效 → 空句柄
ENGINE_INFO("SandboxApp: scene state re-resolved (camera {})",
m_Camera.IsAlive() ? "found" : "missing");
};
// 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复)
Time::SetTimeScale(0.0f);
m_Editor->Init(ctx);
@@ -305,12 +135,6 @@ namespace
private:
std::unique_ptr<Editor> m_Editor = std::make_unique<Editor>();
EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空
EntityHandle m_KeyLight{};
EntityHandle m_Ground{};
EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911)
std::vector<EntityHandle> m_Spinners;
int m_EnvIndex = kDefaultEnv;
};
}