diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 174418c..7e8c0d8 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -367,10 +367,4 @@ void Editor::DoLoadScene() "keeping current", envPath); } } - - // 应用层重解析悬空句柄(相机等沙盒状态) - if (m_Ctx.onSceneLoaded) - { - m_Ctx.onSceneLoaded(); - } } diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h index f7dd0db..41c0273 100644 --- a/App/editor/EditorContext.h +++ b/App/editor/EditorContext.h @@ -12,6 +12,7 @@ #include #include #include +#include #include // kDisplayViewport / kDisplayDirect #include @@ -37,12 +38,12 @@ struct EditorContext { Engine::ECS::Scene *scene = nullptr; Engine::Assets::AssetManager *assets = nullptr; Engine::Application *app = nullptr; + // 编辑器相机(Scene view 专用,归 Renderer 持有——与场景相机实体 + // 无关;面板经此做 F 焦点/Gizmo 视图/对齐跳转) + Engine::EditorCamera *editorCamera = nullptr; std::function getEnvIndex; // 当前环境表下标 std::function setEnvIndex; - // 场景加载成功后由 SandboxApp 重解析悬空句柄(registry 清场重建后 - // 应用层持有的 EntityHandle 全部悬空,见 Scene::Clear 注释) - std::function onSceneLoaded; }; #endif //HELLOVULKAN_EDITORCONTEXT_H diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index c8f10a9..0a997f8 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -198,7 +198,7 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) if (ImGui::Checkbox("primary", &cam->primary) && cam->primary) { // primary 互斥:启用本相机时清掉其余实体的 primary - //(引擎取第一个 primary 作为渲染视角) + //(引擎取第一个 primary 作为游戏形态渲染视角) reg.view().each( [&](entt::entity e, Engine::ECS::CameraComponent& c) { @@ -208,6 +208,18 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) } }); } + + // 对齐跳转:把本实体位姿复制给编辑器相机,Scene view 立即 + // 从该位置观看(只取位姿,不接管渲染——渲染源分派在 + // SceneRenderer,编辑器形态恒用编辑器相机) + if (ImGui::Button("Align Viewport to Camera") && ctx.editorCamera) + { + if (auto* tr = reg.try_get( + selected)) + { + ctx.editorCamera->AlignTo(*tr); + } + } } } diff --git a/App/editor/StatsPanel.cpp b/App/editor/StatsPanel.cpp index ae58bf2..b15bb1c 100644 --- a/App/editor/StatsPanel.cpp +++ b/App/editor/StatsPanel.cpp @@ -19,18 +19,27 @@ void StatsPanel::Draw(const EditorContext& ctx) auto& renderer = *ctx.renderer; auto& app = *ctx.app; - // ---- Camera:主相机位姿(只读遥测;位姿编辑走 Inspector)---- + // ---- Camera:视图相机位姿(只读遥测;跟随渲染源分派)---- { - Engine::ECS::TransformComponent* camT = nullptr; - if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr) + if (ctx.renderer->GetDisplayMode() == kDisplayViewport && ctx.editorCamera) { - const glm::vec3& pos = camT->GetPosition(); - ImGui::Text("Camera pos"); + const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition(); + ImGui::Text("Editor camera pos"); ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); } else { - ImGui::TextDisabled("(no primary camera)"); + Engine::ECS::TransformComponent* camT = nullptr; + if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr) + { + const glm::vec3& pos = camT->GetPosition(); + ImGui::Text("Game camera pos"); + ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); + } + else + { + ImGui::TextDisabled("(no primary camera)"); + } } ImGui::Separator(); } diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 009d624..76aab2e 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -66,6 +66,22 @@ namespace } return p; } + + // 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则, + // 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view + // 专用,与相机实体无关);游戏形态 = primary 相机实体。 + // 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo) + Engine::Camera* ViewCamera(const EditorContext& ctx, + ECS::TransformComponent*& outT) + { + if (ctx.renderer->GetDisplayMode() == kDisplayViewport) + { + outT = &ctx.editorCamera->GetTransform(); + return &ctx.editorCamera->GetController(); + } + auto* cc = ctx.scene->GetPrimaryCamera(&outT); + return cc ? &cc->camera : nullptr; + } } std::optional ViewportPanel::Draw(const EditorContext &ctx, @@ -143,14 +159,10 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, if (ImGui::IsKeyPressed(ImGuiKey_R)) gizmo.op = ImGuizmo::SCALE; if (ImGui::IsKeyPressed(ImGuiKey_T)) gizmo.localSpace = !gizmo.localSpace; if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) { - // F = 焦点选中实体(frame-selection):环绕 pivot 设 - // 为选中实体位置,相机沿视线平移对准(环绕距离沿用 - // 当前值,滚轮可调) - ECS::TransformComponent *camT = nullptr; - if (auto *camComp = ctx.scene->GetPrimaryCamera(&camT); - camComp != nullptr && camT != nullptr) { - camComp->camera.FocusOn(*camT, tr->GetPosition()); - } + // F = 焦点选中实体(frame-selection):编辑器相机 + // 环绕 pivot 设为选中实体位置,沿视线平移对准(环 + // 绕距离沿用当前值,滚轮可调) + ctx.editorCamera->FocusOn(tr->GetPosition()); } } @@ -158,14 +170,16 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, // 选中实体 + 有 Transform 才出杆;矩阵分解直接写回 // TransformComponent(与 Inspector 同源,下一帧数值自动跟随) if (tr != nullptr) { - // 相机矩阵:primary 相机 view + Vulkan proj(转 GL 形式), - // 参数与渲染侧一致(aspect = 渲染分辨率、near/far 0.1/100) + // 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏 + // 形态 = primary 相机实体;与 SceneRenderer::BuildFrame + // 同一分派),参数与渲染侧一致(aspect = 渲染分辨率、 + // near/far 0.1/100) ECS::TransformComponent *camT = nullptr; - if (const auto *camComp = ctx.scene->GetPrimaryCamera(&camT); - camComp != nullptr && camT != nullptr) { - const glm::mat4 view = camComp->camera.GetView(*camT); + if (Engine::Camera *cam = ViewCamera(ctx, camT); + cam != nullptr && camT != nullptr) { + const glm::mat4 view = cam->GetView(*camT); const glm::mat4 projGL = ProjVulkanToGL( - camComp->camera.GetProjection(aspect, 0.1f, 100.0f)); + cam->GetProjection(aspect, 0.1f, 100.0f)); ImGuizmo::SetDrawlist(ImGui::GetWindowDrawList()); // 杆画在 Image 区域内(等比适配后的 item rect) diff --git a/App/main.cpp b/App/main.cpp index 53d3ca8..d9542dc 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -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 // SetInputCapture(Application.h 只前置声明) #include #include -#include -#include -#include -#include -#include #include -#include - -#include -#include +#include #include @@ -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{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{gun}); - m_Gun.AddComponent(); - auto& gt = m_Gun.GetComponent(); - gt.SetPosition(glm::vec3(0.0f, 1.2f, -6.0f)); - // 物理验证:Dynamic 刚体(默认 Box halfExtents 0.5), - // 出生位置悬空 → 应下落至地面静止(Transform 由物理写回) - m_Gun.AddComponent(); - } 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() - .SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose - m_Ground.AddComponent(MeshRendererComponent{ground}); - auto& groundRb = m_Ground.AddComponent(); - 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{gun}); - m_Gun.AddComponent(); - auto& gt = m_Gun.GetComponent(); - 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{model}); - m.GetComponent().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() - .SetPosition(glm::vec3(0.0f, -0.5f, 0.0f)); // dirty → 帧循环统一 Compose - m_Ground.AddComponent(MeshRendererComponent{ground}); - */ - - // 相机实体(primary = 当前渲染视角;位姿存实体 Transform) - m_Camera = scene.CreateEntity("MainCamera"); - auto& camera = m_Camera.AddComponent(); - camera.primary = true; - m_Camera.GetComponent() - .SetPosition(glm::vec3(0.0f, 12.0f, 22.0f)); - m_Camera.GetComponent() - .SetRotation(glm::vec3(-25.0f, 0.0f, 0.0f)); - - // 灯光实体(第一个方向光 = 阴影光源) - m_KeyLight = scene.CreateEntity("KeyLight"); - m_KeyLight.AddComponent( - 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{ - .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().SetPosition(glm::vec3(2.0f, 1.5f, 1.0f)); - fire.AddComponent( - 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().SetPosition(glm::vec3(-2.5f, 2.0f, -2.0f)); - cool.AddComponent( - 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() - .camera.Update(GetWindow(), - m_Camera.GetComponent(), - 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(); - t.SetRotation(glm::vec3( - 0.0f, static_cast(Time::Elapsed()) * 60.0f + - static_cast(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([&](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 m_Editor = std::make_unique(); - - EntityHandle m_Camera{}; // Scene::CreateEntity 之前为空 - EntityHandle m_KeyLight{}; - EntityHandle m_Ground{}; - EntityHandle m_Gun{}; // 骨骼动画验证实体(M1911) - std::vector m_Spinners; int m_EnvIndex = kDefaultEnv; }; } diff --git a/Engine/include/Engine/Renderer/Camera.h b/Engine/include/Engine/Renderer/Camera.h index a63da00..c446b5a 100644 --- a/Engine/include/Engine/Renderer/Camera.h +++ b/Engine/include/Engine/Renderer/Camera.h @@ -54,6 +54,10 @@ namespace Engine { void SetMouseLookEnabled(const bool enabled) { m_MouseLookEnabled = enabled; } bool IsMouseLookEnabled() const { return m_MouseLookEnabled; } + // 清除环绕 pivot(位姿被外部整体改写后 pivot 已失效——下一次 + // MMB 环绕按新视线自动重建) + void ResetOrbit() { m_HasPivot = false; } + private: f32 m_MoveSpeed = 2.5f; f32 m_MouseSensitivity = 0.1f; diff --git a/Engine/include/Engine/Renderer/EditorCamera.h b/Engine/include/Engine/Renderer/EditorCamera.h new file mode 100644 index 0000000..073431d --- /dev/null +++ b/Engine/include/Engine/Renderer/EditorCamera.h @@ -0,0 +1,80 @@ +// +// Created by atdunbg on 2026/10/3. +// +// EditorCamera:Scene view 专用相机(UE FEditorViewportClient 语义)。 +// +// 与场景相机实体是两个东西——它是视口 UI 状态,不属于场景: +// - 位姿自有(内嵌 TransformComponent,无父层级),空场景/无相机 +// 实体也可用,永在不可删 +// - 交互复用 Camera 控制器(RMB 飞行 / MMB 环绕 / 滚轮推拉),输入 +// 直接写回自有 transform——位姿单一事实源不变(transform 即位姿) +// - 渲染源分派在 SceneRenderer::BuildFrame:编辑器形态恒用本类, +// 游戏形态用场景 primary 相机实体(游戏逻辑所有,本类不参与, +// 不 Update 即静止) +// + +#ifndef HELLOVULKAN_EDITORCAMERA_H +#define HELLOVULKAN_EDITORCAMERA_H + +#include +#include +#include + +namespace Engine { + + class Window; + + class EditorCamera { + public: + EditorCamera() + { + // 出场位姿:俯视场中央(与旧沙盒 MainCamera 出场一致, + // 空场景也有可看的初始视角) + m_Transform.SetPosition({0.0f, 12.0f, 22.0f}); + m_Transform.SetRotation({-25.0f, 0.0f, 0.0f}); + m_Transform.Compose(); + } + + // 每帧输入驱动(编辑器形态由 Application 调用;游戏形态不调 + // 即静止)。flyRequiresRMB 恒 true:编辑器移动键只在 RMB 按住 + // 时生效(W/E/R 归 Gizmo,UE 语义)。暂停(timeScale=0)时 + // 仍可飞行——调用方传 unscaled dt + void Update(Window& window, f32 deltaTime) + { + m_Controller.Update(window, m_Transform, deltaTime, + /*flyRequiresRMB=*/true); + m_Transform.Compose(); // World() 是渲染/Gizmo 读源,随写随算 + } + + // 跳转(对齐视口):把相机实体(或任意实体)位姿复制过来, + // Scene view 立即从该位置观看(UE "Pilot This Camera" 的 + // 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建 + void AlignTo(const ECS::TransformComponent& target) + { + m_Transform.SetPosition(target.GetPosition()); + m_Transform.SetRotationQuat(target.GetRotationQuat()); + m_Transform.Compose(); + m_Controller.ResetOrbit(); + } + + // 焦点选中(F 键):pivot 设为 point,沿当前视线平移对准 + void FocusOn(const glm::vec3& point) + { + m_Controller.FocusOn(m_Transform, point); + } + + // 视图 = 自有 transform 世界矩阵的逆(要求已 Compose—— + // Update/AlignTo/ctor 三个写入口都收尾 Compose) + glm::mat4 GetView() const { return m_Controller.GetView(m_Transform); } + + ECS::TransformComponent& GetTransform() { return m_Transform; } + Camera& GetController() { return m_Controller; } + + private: + ECS::TransformComponent m_Transform; // 自有位姿(非场景实体) + Camera m_Controller; // 交互控制器(环绕状态在此) + }; + +} // namespace Engine + +#endif //HELLOVULKAN_EDITORCAMERA_H diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index 25916f7..bcccfcd 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -41,6 +41,7 @@ namespace Engine { class CommandManager; class UniformBuffer; class SceneRenderer; + class EditorCamera; class VulkanAssetUploader; class Cubemap; class IblGenerator; @@ -141,6 +142,9 @@ namespace Engine { // ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)---- int GetDisplayMode() const { return m_DesiredDisplayMode; } void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明) + // 编辑器相机(Scene view 恒用,与场景相机实体无关)。转发 + // SceneRenderer 持有的实例:Application 驱动 Update,面板读写位姿 + EditorCamera& GetEditorCamera(); // cpp 内定义(SceneRenderer 完整类型) // 当帧场景数据(只读直通)。CPU 消费如视口拾取:反投影矩阵必须 // 与渲染同一台相机同一投影(含 [0,1] 深度语义),禁止自行重算 const FrameSceneData& GetFrameSceneData() const; // cpp 内定义 diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index 1d2a5de..07681d1 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -28,6 +28,8 @@ #include #include +#include + namespace Engine { namespace ECS { class Scene; } @@ -35,7 +37,8 @@ namespace Engine { class SceneRenderer { public: - // 从场景构建当帧渲染数据。无 primary 相机返回 false(渲染跳帧)。 + // 从场景构建当帧渲染数据。游戏形态无 primary 相机返回 false + //(渲染跳帧;编辑器形态恒用编辑器相机,不跳)。 // targetExtent 用于 TAA 子像素抖动的 NDC 偏移折算。 // dt 驱动骨骼动画采样推进(crossfade 计时)。 bool BuildFrame(ECS::Scene& scene, Assets::AssetManager& assets, @@ -62,6 +65,9 @@ namespace Engine { void SetLodEnabled(bool b) { m_LodEnabled = b; } void SetDebugView(int v) { m_DebugView = v; } void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport + // 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派); + // Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问 + EditorCamera& GetEditorCamera() { return m_EditorCamera; } // Editor 视口拾取请求(UI 段写入,下一次 BuildFrame 落帧后清除) void SetPickRequest(const glm::vec2& ndc) { @@ -83,6 +89,7 @@ namespace Engine { static void PackIndirect(CullResult& view, bool byMaterial); FrameSceneData m_Frame; + EditorCamera m_EditorCamera; // 编辑器相机(视口 UI 状态,见 GetEditorCamera) PickRequest m_PendingPick{}; // 拾取请求缓冲(BuildFrame 落帧后清除) u32 m_PendingSelected = 0; // 选中实体缓冲(描边) diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index 81f5087..6e57d8d 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -233,6 +234,11 @@ namespace Engine m_SceneRenderer->SetDisplayMode(mode); } + EditorCamera& Renderer::GetEditorCamera() + { + return m_SceneRenderer->GetEditorCamera(); + } + const FrameSceneData &Renderer::GetFrameSceneData() const { return m_SceneRenderer->Frame(); diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index c45d0ec..8d70775 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -258,14 +259,21 @@ namespace Engine ubo.prevView = m_LastView; ubo.prevProj = m_LastProjNoJitter; - // ---- 1. 相机(位姿单一事实源 = 相机实体的 Transform)---- - TransformComponent* camTransform = nullptr; - auto* cameraComp = scene.GetPrimaryCamera(&camTransform); - if (!cameraComp || !camTransform) { - // 无相机:本帧跳过(正常场景必有相机,安全网) + // ---- 1. 相机:渲染源按显示模式分派(UE 语义)。编辑器形态 + // Scene view 恒用编辑器相机(视口 UI 状态,空场景可用); + // 游戏形态用场景 primary 相机实体(游戏逻辑所有)---- + ECS::TransformComponent* camTransform = nullptr; + const Camera* camera = nullptr; + if (m_DisplayMode == kDisplayViewport) { + camTransform = &m_EditorCamera.GetTransform(); + camera = &m_EditorCamera.GetController(); + } else if (auto* cameraComp = scene.GetPrimaryCamera(&camTransform)) { + camera = &cameraComp->camera; + } + if (!camera || !camTransform) { + // 游戏形态无 primary 相机:本帧跳过(安全网) return false; // 拾取请求未消费,留待下一帧重试 } - const Camera& camera = cameraComp->camera; // 拾取请求落帧(一次性消费:本帧 HitProxy 渲染 + 像素拷贝)。 // 放在相机校验之后:跳帧时请求保留,不丢点击 @@ -276,8 +284,8 @@ namespace Engine const f32 aspect = static_cast(targetExtent.width) / static_cast(targetExtent.height); - ubo.view = camera.GetView(*camTransform); - ubo.proj = camera.GetProjection(aspect, 0.1f, 100.0f); + ubo.view = camera->GetView(*camTransform); + ubo.proj = camera->GetProjection(aspect, 0.1f, 100.0f); // 未抖动投影:velocity 的当前帧 clip 位置用它(抖动不能进 velocity) ubo.projNoJitter = ubo.proj;