From 35b394ee37790320c0f2e3f41ae2b21b079cb282 Mon Sep 17 00:00:00 2001 From: ayuan9957 <107920784+ayuan9957@users.noreply.github.com> Date: Wed, 17 Jun 2026 16:16:40 +0800 Subject: [PATCH] feat(editor): refine scene workflow and viewport grid --- CMakeLists.txt | 23 ++ .../Private/MetaCoreBuiltinEditorModule.cpp | 242 +++++++++++------- .../Private/MetaCoreEditorApp.cpp | 238 ++++++++++++++--- .../Public/MetaCoreEditor/MetaCoreEditorApp.h | 3 + .../Private/MetaCoreWindow.cpp | 4 + .../MetaCoreEditorViewportRenderer.cpp | 7 + .../Private/MetaCoreFilamentSceneBridge.cpp | 234 +++++++++++++++++ Source/MetaCoreRender/Private/editorGrid.mat | 83 ++++++ .../MetaCoreEditorViewportRenderer.h | 2 + .../MetaCoreFilamentSceneBridge.h | 1 + tests/MetaCoreSmokeTests.cpp | 132 ++++++++++ 11 files changed, 831 insertions(+), 138 deletions(-) create mode 100644 Source/MetaCoreRender/Private/editorGrid.mat diff --git a/CMakeLists.txt b/CMakeLists.txt index a66e38c..48ab773 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,8 @@ endif() set(METACORE_UI_BLIT_MATERIAL_SOURCE "${CMAKE_SOURCE_DIR}/third_party/filament/libs/filagui/src/materials/uiBlit.mat") set(METACORE_UI_BLIT_MATERIAL_OUTPUT "${CMAKE_BINARY_DIR}/uiBlit.filamat") +set(METACORE_EDITOR_GRID_MATERIAL_SOURCE "${CMAKE_SOURCE_DIR}/Source/MetaCoreRender/Private/editorGrid.mat") +set(METACORE_EDITOR_GRID_MATERIAL_OUTPUT "${CMAKE_BINARY_DIR}/editorGrid.filamat") set(METACORE_FILAMENT_MATC "${METACORE_FILAMENT_ROOT}/bin/matc.exe") add_custom_command( @@ -81,12 +83,33 @@ add_custom_target(MetaCoreUiBlitMaterial DEPENDS "${METACORE_UI_BLIT_MATERIAL_OUTPUT}" ) +add_custom_command( + OUTPUT "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}" + COMMAND "${METACORE_FILAMENT_MATC}" + --platform desktop + --api all + --output "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}" + "${METACORE_EDITOR_GRID_MATERIAL_SOURCE}" + DEPENDS + "${METACORE_EDITOR_GRID_MATERIAL_SOURCE}" + "${METACORE_FILAMENT_MATC}" + VERBATIM +) + +add_custom_target(MetaCoreEditorGridMaterial + DEPENDS "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}" +) + function(metacore_stage_ui_blit_material target_name) add_dependencies(${target_name} MetaCoreUiBlitMaterial) + add_dependencies(${target_name} MetaCoreEditorGridMaterial) add_custom_command(TARGET ${target_name} POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy_if_different "${METACORE_UI_BLIT_MATERIAL_OUTPUT}" "$/uiBlit.filamat" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${METACORE_EDITOR_GRID_MATERIAL_OUTPUT}" + "$/editorGrid.filamat" VERBATIM ) endfunction() diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp index fc6cf56..fbc44e3 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinEditorModule.cpp @@ -742,70 +742,6 @@ void MetaCoreReimportProjectAsset(MetaCoreEditorContext& editorContext, const Me } } -void MetaCoreEnsureProjectStartupScene(MetaCoreEditorContext& editorContext) { - const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); - const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); - if (assetDatabaseService == nullptr || scenePersistenceService == nullptr || !assetDatabaseService->HasProject()) { - return; - } - - if (scenePersistenceService->LoadStartupScene(editorContext)) { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Project", "已加载项目启动场景"); - return; - } - - if (!assetDatabaseService->GetProjectDescriptor().ScenePaths.empty()) { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Project", "启动场景不可用,已保留当前场景"); - return; - } - - editorContext.GetScene().RestoreSnapshot(MetaCoreCreateDefaultScene().CaptureSnapshot()); - editorContext.ClearSelection(); - const std::filesystem::path bootstrapScenePath = std::filesystem::path("Scenes") / "Main.mcscene.json"; - if (scenePersistenceService->SaveSceneAs(editorContext, bootstrapScenePath)) { - (void)assetDatabaseService->SetStartupScenePath(bootstrapScenePath); - editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Project", "已为新项目创建默认启动场景"); - } else { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Warning, "Project", "项目已打开,但默认启动场景创建失败"); - } -} - -void MetaCoreOpenProject(MetaCoreEditorContext& editorContext, const std::filesystem::path& projectRoot) { - const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); - if (assetDatabaseService == nullptr) { - return; - } - - if (assetDatabaseService->OpenProject(projectRoot)) { - MetaCoreEnsureProjectStartupScene(editorContext); - editorContext.AddConsoleMessage( - MetaCoreLogLevel::Info, - "Project", - "已打开项目: " + assetDatabaseService->GetProjectDescriptor().RootPath.string() - ); - } else { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "打开项目失败"); - } -} - -void MetaCoreCreateProject(MetaCoreEditorContext& editorContext, const std::filesystem::path& projectRoot, const std::string& projectName) { - const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); - if (assetDatabaseService == nullptr) { - return; - } - - if (assetDatabaseService->CreateProject(projectRoot, projectName)) { - MetaCoreEnsureProjectStartupScene(editorContext); - editorContext.AddConsoleMessage( - MetaCoreLogLevel::Info, - "Project", - "已创建项目: " + assetDatabaseService->GetProjectDescriptor().RootPath.string() - ); - } else { - editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Project", "创建项目失败"); - } -} - void MetaCoreHandleGlobalEditorShortcuts(MetaCoreEditorContext& editorContext) { const ImGuiIO& io = ImGui::GetIO(); if (io.WantTextInput) { @@ -2537,25 +2473,27 @@ public: const auto scenePersistenceService = editorContext.GetModuleRegistry().ResolveService(); const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); - // 顶级菜单 1:项目 (对应 Project) - if (ImGui::BeginMenu("项目")) { - if (ImGui::MenuItem("新建项目...")) { - if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { - const auto& project = assetDatabaseService->GetProjectDescriptor(); - std::snprintf(CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_), "%s", project.RootPath.parent_path().string().c_str()); - } else { - std::snprintf(CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_), "%s", std::filesystem::current_path().string().c_str()); - } - std::snprintf(CreateProjectNameBuffer_, sizeof(CreateProjectNameBuffer_), "%s", "NewMetaCoreProject"); - ImGui::OpenPopup("MetaCoreCreateProjectPopup"); + // 顶级菜单 1:场景。项目创建/打开由 Hub 负责,Editor 内只处理当前项目中的场景。 + if (ImGui::BeginMenu("场景")) { + if (ImGui::MenuItem("新建场景...", nullptr, false, assetDatabaseService != nullptr && assetDatabaseService->HasProject())) { + std::snprintf(NewScenePathBuffer_, sizeof(NewScenePathBuffer_), "%s", "Scenes/NewScene.mcscene.json"); + ImGui::OpenPopup("MetaCoreCreateScenePopup"); } - if (ImGui::MenuItem("打开项目...")) { - if (assetDatabaseService != nullptr && assetDatabaseService->HasProject()) { - std::snprintf(OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_), "%s", assetDatabaseService->GetProjectDescriptor().RootPath.string().c_str()); - } else { - std::snprintf(OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_), "%s", std::filesystem::current_path().string().c_str()); + if (ImGui::MenuItem("打开场景...", nullptr, false, scenePersistenceService != nullptr && assetDatabaseService != nullptr && assetDatabaseService->HasProject())) { + const std::filesystem::path defaultScenePath = scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene() + ? scenePersistenceService->GetCurrentScenePath() + : std::filesystem::path("Scenes") / "Main.mcscene.json"; + std::snprintf(OpenScenePathBuffer_, sizeof(OpenScenePathBuffer_), "%s", defaultScenePath.generic_string().c_str()); + ImGui::OpenPopup("MetaCoreOpenScenePopup"); + } + if (scenePersistenceService != nullptr && ImGui::BeginMenu("项目场景", !scenePersistenceService->GetKnownScenePaths().empty())) { + for (const std::filesystem::path& scenePath : scenePersistenceService->GetKnownScenePaths()) { + if (ImGui::MenuItem(scenePath.generic_string().c_str())) { + PendingScenePath_ = scenePath; + RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::OpenScene); + } } - ImGui::OpenPopup("MetaCoreOpenProjectPopup"); + ImGui::EndMenu(); } ImGui::Separator(); if (ImGui::MenuItem("保存场景", "Ctrl+S", false, scenePersistenceService != nullptr)) { @@ -2594,20 +2532,19 @@ public: ImGui::MenuItem("当前运行模式:MetaCore 编辑器", nullptr, false, false); ImGui::EndMenu(); } - + if (ImGui::MenuItem("退出")) { - editorContext.GetWindow().RequestClose(); + RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::Exit); } ImGui::EndMenu(); } - // 新建项目弹窗中文化 - if (ImGui::BeginPopupModal("MetaCoreCreateProjectPopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::InputText("项目目录", CreateProjectPathBuffer_, sizeof(CreateProjectPathBuffer_)); - ImGui::InputText("项目名称", CreateProjectNameBuffer_, sizeof(CreateProjectNameBuffer_)); + if (ImGui::BeginPopupModal("MetaCoreCreateScenePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextDisabled("相对于当前项目根目录"); + ImGui::InputText("场景路径", NewScenePathBuffer_, sizeof(NewScenePathBuffer_)); if (ImGui::Button("创建")) { - const std::filesystem::path projectRoot = std::filesystem::path(CreateProjectPathBuffer_) / std::filesystem::path(CreateProjectNameBuffer_); - MetaCoreCreateProject(editorContext, projectRoot, CreateProjectNameBuffer_); + PendingScenePath_ = std::filesystem::path(NewScenePathBuffer_); + RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::NewScene); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); @@ -2617,11 +2554,12 @@ public: ImGui::EndPopup(); } - // 打开项目弹窗中文化 - if (ImGui::BeginPopupModal("MetaCoreOpenProjectPopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { - ImGui::InputText("项目路径", OpenProjectPathBuffer_, sizeof(OpenProjectPathBuffer_)); + if (ImGui::BeginPopupModal("MetaCoreOpenScenePopup", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + ImGui::TextDisabled("相对于当前项目根目录"); + ImGui::InputText("场景路径", OpenScenePathBuffer_, sizeof(OpenScenePathBuffer_)); if (ImGui::Button("打开")) { - MetaCoreOpenProject(editorContext, std::filesystem::path(OpenProjectPathBuffer_)); + PendingScenePath_ = std::filesystem::path(OpenScenePathBuffer_); + RequestSceneMenuAction(editorContext, scenePersistenceService, PendingSceneAction::OpenScene); ImGui::CloseCurrentPopup(); } ImGui::SameLine(); @@ -2631,6 +2569,8 @@ public: ImGui::EndPopup(); } + DrawPendingSceneActionPopup(editorContext, scenePersistenceService); + // 顶级菜单 2:编辑 (对应 Edit) if (ImGui::BeginMenu("编辑")) { if (ImGui::MenuItem("撤销", "Ctrl+Z", false, editorContext.GetCommandService().CanUndo())) { @@ -2701,9 +2641,119 @@ public: } private: - char CreateProjectPathBuffer_[512]{}; - char CreateProjectNameBuffer_[256]{}; - char OpenProjectPathBuffer_[512]{}; + enum class PendingSceneAction { + None, + NewScene, + OpenScene, + Exit + }; + + void RequestSceneMenuAction( + MetaCoreEditorContext& editorContext, + const std::shared_ptr& scenePersistenceService, + PendingSceneAction action + ) { + if (action == PendingSceneAction::None) { + return; + } + + if (scenePersistenceService == nullptr || !scenePersistenceService->IsSceneDirty()) { + ExecuteSceneMenuAction(editorContext, scenePersistenceService, action); + return; + } + + PendingAction_ = action; + OpenPendingActionPopup_ = true; + } + + void ExecuteSceneMenuAction( + MetaCoreEditorContext& editorContext, + const std::shared_ptr& scenePersistenceService, + PendingSceneAction action + ) { + switch (action) { + case PendingSceneAction::NewScene: + CreateAndOpenScene(editorContext, scenePersistenceService); + break; + case PendingSceneAction::OpenScene: + if (scenePersistenceService != nullptr && !PendingScenePath_.empty()) { + (void)scenePersistenceService->LoadScene(editorContext, PendingScenePath_); + } + break; + case PendingSceneAction::Exit: + editorContext.GetWindow().RequestClose(); + break; + case PendingSceneAction::None: + break; + } + + PendingAction_ = PendingSceneAction::None; + PendingScenePath_.clear(); + } + + void DrawPendingSceneActionPopup( + MetaCoreEditorContext& editorContext, + const std::shared_ptr& scenePersistenceService + ) { + if (OpenPendingActionPopup_) { + ImGui::OpenPopup("Scene Action Unsaved Scene Changes"); + OpenPendingActionPopup_ = false; + } + + if (ImGui::BeginPopupModal("Scene Action Unsaved Scene Changes", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + const std::string currentScene = scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene() + ? scenePersistenceService->GetCurrentSceneDisplayName() + : std::string("Untitled"); + + ImGui::Text("Save changes to %s before continuing?", currentScene.c_str()); + ImGui::Separator(); + + if (ImGui::Button("Save and Continue", ImVec2(150, 0))) { + if (scenePersistenceService == nullptr || scenePersistenceService->SaveCurrentScene(editorContext)) { + ExecuteSceneMenuAction(editorContext, scenePersistenceService, PendingAction_); + ImGui::CloseCurrentPopup(); + } else { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "保存失败,已取消操作"); + } + } + ImGui::SameLine(); + if (ImGui::Button("Don't Save", ImVec2(110, 0))) { + ExecuteSceneMenuAction(editorContext, scenePersistenceService, PendingAction_); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(90, 0))) { + PendingAction_ = PendingSceneAction::None; + PendingScenePath_.clear(); + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + } + + void CreateAndOpenScene( + MetaCoreEditorContext& editorContext, + const std::shared_ptr& scenePersistenceService + ) { + const auto assetDatabaseService = editorContext.GetModuleRegistry().ResolveService(); + if (assetDatabaseService == nullptr || scenePersistenceService == nullptr || PendingScenePath_.empty()) { + return; + } + + const auto createdScene = assetDatabaseService->CreateSceneAsset(PendingScenePath_, false); + if (!createdScene.has_value()) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "创建场景失败: " + PendingScenePath_.generic_string()); + return; + } + + (void)scenePersistenceService->LoadScene(editorContext, createdScene->RelativePath); + } + + char NewScenePathBuffer_[512]{}; + char OpenScenePathBuffer_[512]{}; + std::filesystem::path PendingScenePath_{}; + PendingSceneAction PendingAction_ = PendingSceneAction::None; + bool OpenPendingActionPopup_ = false; }; class MetaCoreHierarchyPanelProvider final : public MetaCoreIEditorPanelProvider { diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index 22e92cd..3f27231 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -297,9 +298,9 @@ bool MetaCoreProjectWorldPointToViewport( } void MetaCoreDrawWorldOriginOverlay( - const MetaCoreEditorContext& editorContext, - const MetaCoreSceneView& sceneView, - const MetaCoreSceneViewportState& viewportState + const MetaCoreEditorContext&, + const MetaCoreSceneView&, + const MetaCoreSceneViewportState& ) { // 用户要求去掉小 XYZ 轴和原点指示 return; @@ -307,62 +308,161 @@ void MetaCoreDrawWorldOriginOverlay( void MetaCoreDrawViewportGridOverlay( const MetaCoreEditorContext& editorContext, + const MetaCoreSceneView& sceneView, const MetaCoreSceneViewportState& viewportState ) { - if (!editorContext.GetShowViewportGrid()) { + (void)editorContext; + (void)sceneView; + (void)viewportState; + // The editor grid is rendered by Filament as a real 3D ground plane. + return; + + if (!editorContext.GetShowViewportGrid() || viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { return; } - if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { - return; + + constexpr float gridY = 0.0F; + constexpr float gridSpacing = 1.0F; + constexpr int majorLineInterval = 5; + constexpr int maxVisibleLines = 240; + constexpr float fallbackHalfExtent = 80.0F; + + const glm::mat4 viewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp); + const glm::mat4 projectionMatrix = glm::perspective( + glm::radians(sceneView.VerticalFieldOfViewDegrees), + viewportState.Width / viewportState.Height, + 0.05F, + 500.0F + ); + const glm::mat4 inverseViewProjection = glm::inverse(projectionMatrix * viewMatrix); + + const auto raycastGroundAtViewportPoint = [&](float screenX, float screenY, glm::vec3& groundPoint) -> bool { + const float ndcX = ((screenX - viewportState.Left) / viewportState.Width) * 2.0F - 1.0F; + const float ndcY = 1.0F - ((screenY - viewportState.Top) / viewportState.Height) * 2.0F; + glm::vec4 nearPoint = inverseViewProjection * glm::vec4(ndcX, ndcY, -1.0F, 1.0F); + glm::vec4 farPoint = inverseViewProjection * glm::vec4(ndcX, ndcY, 1.0F, 1.0F); + if (std::abs(nearPoint.w) <= 0.0001F || std::abs(farPoint.w) <= 0.0001F) { + return false; + } + nearPoint /= nearPoint.w; + farPoint /= farPoint.w; + const glm::vec3 rayOrigin = glm::vec3(nearPoint); + const glm::vec3 rayDirection = glm::normalize(glm::vec3(farPoint - nearPoint)); + if (std::abs(rayDirection.y) <= 0.0001F) { + return false; + } + const float t = (gridY - rayOrigin.y) / rayDirection.y; + if (t <= 0.0F) { + return false; + } + groundPoint = rayOrigin + rayDirection * t; + return true; + }; + + std::vector groundPoints; + groundPoints.reserve(9); + for (int yStep = 0; yStep <= 2; ++yStep) { + for (int xStep = 0; xStep <= 2; ++xStep) { + glm::vec3 groundPoint{}; + const float screenX = viewportState.Left + viewportState.Width * (static_cast(xStep) * 0.5F); + const float screenY = viewportState.Top + viewportState.Height * (static_cast(yStep) * 0.5F); + if (raycastGroundAtViewportPoint(screenX, screenY, groundPoint)) { + groundPoints.push_back(groundPoint); + } + } + } + + float minX = sceneView.CameraTarget.x - fallbackHalfExtent; + float maxX = sceneView.CameraTarget.x + fallbackHalfExtent; + float minZ = sceneView.CameraTarget.z - fallbackHalfExtent; + float maxZ = sceneView.CameraTarget.z + fallbackHalfExtent; + if (!groundPoints.empty()) { + minX = maxX = groundPoints.front().x; + minZ = maxZ = groundPoints.front().z; + for (const glm::vec3& point : groundPoints) { + minX = std::min(minX, point.x); + maxX = std::max(maxX, point.x); + minZ = std::min(minZ, point.z); + maxZ = std::max(maxZ, point.z); + } + const float padding = std::max(gridSpacing * 4.0F, glm::distance(sceneView.CameraPosition, sceneView.CameraTarget) * 0.08F); + minX -= padding; + maxX += padding; + minZ -= padding; + maxZ += padding; + } + + int startX = static_cast(std::floor(minX / gridSpacing)); + int endX = static_cast(std::ceil(maxX / gridSpacing)); + int startZ = static_cast(std::floor(minZ / gridSpacing)); + int endZ = static_cast(std::ceil(maxZ / gridSpacing)); + if (endX - startX > maxVisibleLines) { + const int center = static_cast(std::floor(sceneView.CameraTarget.x / gridSpacing)); + startX = center - maxVisibleLines / 2; + endX = center + maxVisibleLines / 2; + } + if (endZ - startZ > maxVisibleLines) { + const int center = static_cast(std::floor(sceneView.CameraTarget.z / gridSpacing)); + startZ = center - maxVisibleLines / 2; + endZ = center + maxVisibleLines / 2; } ImDrawList* drawList = ImGui::GetForegroundDrawList(); const ImVec2 topLeft(viewportState.Left, viewportState.Top); const ImVec2 bottomRight(viewportState.Left + viewportState.Width, viewportState.Top + viewportState.Height); - const ImVec2 center((topLeft.x + bottomRight.x) * 0.5F, (topLeft.y + bottomRight.y) * 0.5F); + const ImU32 minorColor = IM_COL32(150, 165, 180, 18); + const ImU32 majorColor = IM_COL32(180, 195, 210, 30); + const ImU32 axisXColor = IM_COL32(220, 85, 85, 65); + const ImU32 axisZColor = IM_COL32(85, 145, 230, 65); - const float minorSpacing = 32.0F; - const int verticalMinorCount = static_cast(viewportState.Width / minorSpacing) + 2; - const int horizontalMinorCount = static_cast(viewportState.Height / minorSpacing) + 2; - const ImU32 minorColor = IM_COL32(110, 120, 135, 40); - const ImU32 majorColor = IM_COL32(150, 165, 185, 70); - const ImU32 axisXColor = IM_COL32(220, 80, 80, 95); - const ImU32 axisYColor = IM_COL32(120, 210, 100, 95); - - for (int lineIndex = -verticalMinorCount; lineIndex <= verticalMinorCount; ++lineIndex) { - const float x = center.x + static_cast(lineIndex) * minorSpacing; - if (x < topLeft.x || x > bottomRight.x) { - continue; + const auto drawProjectedGroundLine = [&](const glm::vec3& start, const glm::vec3& end, ImU32 color, float thickness) { + constexpr int segmentCount = 48; + ImVec2 previousPoint{}; + bool previousVisible = false; + for (int segmentIndex = 0; segmentIndex <= segmentCount; ++segmentIndex) { + const float t = static_cast(segmentIndex) / static_cast(segmentCount); + const glm::vec3 worldPoint = start + (end - start) * t; + ImVec2 screenPoint{}; + glm::vec3 projectedPixelGroundPoint{}; + const bool visible = MetaCoreProjectWorldPointToViewport(sceneView, viewportState, worldPoint, screenPoint) + && raycastGroundAtViewportPoint(screenPoint.x, screenPoint.y, projectedPixelGroundPoint); + if (visible && previousVisible) { + drawList->AddLine(previousPoint, screenPoint, color, thickness); + } + previousPoint = screenPoint; + previousVisible = visible; } - const bool isMajor = (lineIndex % 4) == 0; - drawList->AddLine( - ImVec2(x, topLeft.y), - ImVec2(x, bottomRight.y), - lineIndex == 0 ? axisXColor : (isMajor ? majorColor : minorColor), - lineIndex == 0 ? 1.8F : (isMajor ? 1.2F : 1.0F) + }; + + drawList->PushClipRect(topLeft, bottomRight, true); + for (int xLine = startX; xLine <= endX; ++xLine) { + const bool isMajor = (xLine % majorLineInterval) == 0; + const float x = static_cast(xLine) * gridSpacing; + drawProjectedGroundLine( + glm::vec3(x, gridY, static_cast(startZ) * gridSpacing), + glm::vec3(x, gridY, static_cast(endZ) * gridSpacing), + xLine == 0 ? axisZColor : (isMajor ? majorColor : minorColor), + xLine == 0 ? 1.4F : 1.0F ); } - - for (int lineIndex = -horizontalMinorCount; lineIndex <= horizontalMinorCount; ++lineIndex) { - const float y = center.y + static_cast(lineIndex) * minorSpacing; - if (y < topLeft.y || y > bottomRight.y) { - continue; - } - const bool isMajor = (lineIndex % 4) == 0; - drawList->AddLine( - ImVec2(topLeft.x, y), - ImVec2(bottomRight.x, y), - lineIndex == 0 ? axisYColor : (isMajor ? majorColor : minorColor), - lineIndex == 0 ? 1.8F : (isMajor ? 1.2F : 1.0F) + for (int zLine = startZ; zLine <= endZ; ++zLine) { + const bool isMajor = (zLine % majorLineInterval) == 0; + const float z = static_cast(zLine) * gridSpacing; + drawProjectedGroundLine( + glm::vec3(static_cast(startX) * gridSpacing, gridY, z), + glm::vec3(static_cast(endX) * gridSpacing, gridY, z), + zLine == 0 ? axisXColor : (isMajor ? majorColor : minorColor), + zLine == 0 ? 1.4F : 1.0F ); } + drawList->PopClipRect(); } void MetaCoreDrawSelectionBoundsOverlay( - const MetaCoreEditorContext& editorContext, - const MetaCoreScene& scene, - const MetaCoreSceneView& sceneView, - const MetaCoreSceneViewportState& viewportState + const MetaCoreEditorContext&, + const MetaCoreScene&, + const MetaCoreSceneView&, + const MetaCoreSceneViewportState& ) { // 用户要求去掉左下角的黄色框和文字 return; @@ -716,6 +816,10 @@ bool MetaCoreEditorApp::Initialize(int argc, char* argv[]) { ChangeWindowMessageFilter(0x0049, MSGFLT_ADD); // WM_COPYGLOBALMEM Window_.SetNativeWindowMessageHandler([this](void* nativeWindowHandle, unsigned int message, std::uintptr_t wparam, std::intptr_t lparam) { + if (message == WM_CLOSE) { + return RequestEditorCloseWithSavePrompt(); + } + if (message == WM_DROPFILES) { HDROP hDrop = reinterpret_cast(wparam); UINT fileCount = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); @@ -906,6 +1010,7 @@ int MetaCoreEditorApp::Run() { // 1. 收集并更新场景数据(但不在这里渲染) MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView(); sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId(); + ViewportRenderer_.SetEditorGridVisible(EditorContext_->GetShowViewportGrid()); ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView); // 2. 统一在一个 Frame 内渲染离屏 3D 和上屏 UI @@ -1128,6 +1233,54 @@ void MetaCoreEditorApp::DrawEditorFrame() { DrawPlaceholderDockWindow("动画状态机编辑器###animfsm_editor", "动画状态机编辑器"); ApplyPendingDockTabSelections(); DrawEditorStatusBar(); + DrawPendingExitPopup(); + } +} + +bool MetaCoreEditorApp::RequestEditorCloseWithSavePrompt() { + if (const auto scenePersistenceService = ModuleRegistry_.ResolveService(); + scenePersistenceService != nullptr && scenePersistenceService->IsSceneDirty()) { + PendingExitPopup_ = true; + return true; + } + + Window_.RequestClose(); + return true; +} + +void MetaCoreEditorApp::DrawPendingExitPopup() { + if (PendingExitPopup_) { + ImGui::OpenPopup("Exit Unsaved Scene Changes"); + PendingExitPopup_ = false; + } + + if (ImGui::BeginPopupModal("Exit Unsaved Scene Changes", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { + const auto scenePersistenceService = ModuleRegistry_.ResolveService(); + const std::string currentScene = scenePersistenceService != nullptr && scenePersistenceService->HasOpenScene() + ? scenePersistenceService->GetCurrentSceneDisplayName() + : std::string("Untitled"); + + ImGui::Text("Save changes to %s before exiting?", currentScene.c_str()); + ImGui::Separator(); + + if (ImGui::Button("Save and Exit", ImVec2(130, 0))) { + if (scenePersistenceService == nullptr || scenePersistenceService->SaveCurrentScene(*EditorContext_)) { + Window_.RequestClose(); + ImGui::CloseCurrentPopup(); + } else { + EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Error, "Scene", "保存失败,已取消退出"); + } + } + ImGui::SameLine(); + if (ImGui::Button("Don't Save", ImVec2(110, 0))) { + Window_.RequestClose(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(90, 0))) { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); } } @@ -1601,6 +1754,7 @@ void MetaCoreEditorApp::DrawSceneViewWindow() { MetaCoreSceneView sceneView = EditorContext_->GetCameraController().BuildSceneView(); sceneView.SelectedObjectId = EditorContext_->GetSelectedObjectId(); + ViewportRenderer_.SetEditorGridVisible(EditorContext_->GetShowViewportGrid()); ViewportRenderer_.RenderSceneToViewport(Scene_, sceneView); if (void* texPtr = ViewportRenderer_.GetFilamentTexturePointer(); texPtr != nullptr) { @@ -1753,7 +1907,7 @@ void MetaCoreEditorApp::DrawSceneViewWindow() { } } - MetaCoreDrawViewportGridOverlay(*EditorContext_, viewportState); + MetaCoreDrawViewportGridOverlay(*EditorContext_, sceneView, viewportState); MetaCoreDrawWorldOriginOverlay(*EditorContext_, sceneView, viewportState); MetaCoreDrawSelectionBoundsOverlay(*EditorContext_, Scene_, sceneView, viewportState); MetaCoreDrawSelectionHierarchyOverlay(*EditorContext_, Scene_, sceneView, viewportState); diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h index 5f70816..d23319f 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h @@ -34,6 +34,8 @@ private: void DrawEditorFrame(); void DrawEditorToolbar(); void DrawEditorStatusBar(); + void DrawPendingExitPopup(); + bool RequestEditorCloseWithSavePrompt(); void DrawSceneViewWindow(); void DrawGameViewWindow(); void DrawPlaceholderDockWindow(const char* label, const char* caption); @@ -56,6 +58,7 @@ private: std::filesystem::path ImGuiLayoutProjectRoot_{}; std::filesystem::path ViewportProjectRoot_{}; std::string ImGuiIniPath_{}; + bool PendingExitPopup_ = false; bool Initialized_ = false; }; diff --git a/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp b/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp index 4abc60d..92a2e8a 100644 --- a/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp +++ b/Source/MetaCorePlatform/Private/MetaCoreWindow.cpp @@ -112,6 +112,10 @@ LRESULT CALLBACK MetaCoreWindowSubclassProc(HWND hwnd, UINT msg, WPARAM wparam, handledByEditor = owner.MessageHandler(hwnd, msg, static_cast(wparam), static_cast(lparam)); } + if (msg == WM_CLOSE && handledByEditor) { + return 1; + } + if (msg == WM_MOUSEWHEEL) { owner.Input.AddMouseWheelDelta(static_cast(GET_WHEEL_DELTA_WPARAM(wparam)) / static_cast(WHEEL_DELTA)); } else if (msg == WM_CLOSE) { diff --git a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp index 805e5e2..19bffb6 100644 --- a/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreEditorViewportRenderer.cpp @@ -79,6 +79,12 @@ void MetaCoreEditorViewportRenderer::SetViewportRect(const MetaCoreViewportRect& } } +void MetaCoreEditorViewportRenderer::SetEditorGridVisible(bool visible) { + (void)visible; + EditorGridVisible_ = false; + FilamentSceneBridge_.SetEditorGridVisible(false); +} + void MetaCoreEditorViewportRenderer::RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera) { if (RenderDevice_ == nullptr) { return; @@ -88,6 +94,7 @@ void MetaCoreEditorViewportRenderer::RenderSceneToViewport(MetaCoreScene& scene, // 动态调整离屏纹理大小以匹配 ImGui 视口 FilamentSceneBridge_.Resize(static_cast(ViewportRect_.Width), static_cast(ViewportRect_.Height)); + FilamentSceneBridge_.SetEditorGridVisible(false); // 更新并同步 Filament 桥接器(但不在这里调用 Render,交由 RenderAll 统一渲染) FilamentSceneBridge_.ApplySceneView(sceneView); diff --git a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp index c7ab652..e94f1ef 100644 --- a/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp +++ b/Source/MetaCoreRender/Private/MetaCoreFilamentSceneBridge.cpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -46,7 +48,10 @@ #include #include #include +#include #include +#include +#include #include #include #include @@ -70,6 +75,45 @@ namespace MetaCore { namespace { +struct MetaCoreEditorGridVertex { + float X; + float Y; + float Z; +}; + +[[nodiscard]] std::filesystem::path MetaCoreGetExecutableDirectory() { + std::array executablePath{}; + const DWORD length = GetModuleFileNameW( + nullptr, + executablePath.data(), + static_cast(executablePath.size()) + ); + if (length == 0 || length >= executablePath.size()) { + return {}; + } + return std::filesystem::path(executablePath.data()).parent_path(); +} + +[[nodiscard]] std::ifstream MetaCoreOpenEditorGridMaterial() { + std::vector candidates{}; + + const std::filesystem::path executableDirectory = MetaCoreGetExecutableDirectory(); + if (!executableDirectory.empty()) { + candidates.push_back(executableDirectory / "editorGrid.filamat"); + } + candidates.emplace_back("editorGrid.filamat"); + candidates.emplace_back("../editorGrid.filamat"); + candidates.emplace_back("build/editorGrid.filamat"); + + for (const std::filesystem::path& candidate : candidates) { + std::ifstream file(candidate, std::ios::binary); + if (file.is_open()) { + return file; + } + } + return {}; +} + [[nodiscard]] filament::backend::Backend MetaCoreToFilamentBackend(MetaCoreRenderBackend backend) { switch (backend) { case MetaCoreRenderBackend::OpenGL: @@ -423,6 +467,7 @@ public: // 开启高级后处理 View_->setAntiAliasing(filament::View::AntiAliasing::FXAA); View_->setBloomOptions({ .strength = 0.15f, .resolution = 384, .levels = 6, .blendMode = filament::View::BloomOptions::BlendMode::ADD, .threshold = true }); + CreateEditorGridResources(); std::cout << "FilamentSceneBridge: Initialized with Offscreen=" << (offscreen ? "true" : "false") << std::endl; return true; @@ -431,6 +476,7 @@ public: void Shutdown() { if (Engine_) { std::cout << "FilamentSceneBridge: Shutting down..." << std::endl; + DestroyEditorGridResources(); // 1. 销毁所有加载的资产,切断其内部 Renderable 对克隆材质实例的占用引用 std::cout << "[DEBUG] LoadedAssets_ size: " << LoadedAssets_.size() << std::endl; @@ -579,6 +625,181 @@ public: ProjectRootPath_ = projectRootPath; } + void SetEditorGridVisible(bool visible) { + EditorGridVisible_ = visible; + SyncEditorGridVisibility(); + } + + void CreateEditorGridResources() { + if (Engine_ == nullptr || Scene_ == nullptr || EditorGridEntity_) { + return; + } + + std::ifstream materialFile = MetaCoreOpenEditorGridMaterial(); + if (!materialFile.is_open()) { + std::cerr << "FilamentSceneBridge: editorGrid.filamat not found; viewport grid disabled." << std::endl; + return; + } + + std::vector materialBuffer( + (std::istreambuf_iterator(materialFile)), + std::istreambuf_iterator() + ); + if (materialBuffer.empty()) { + std::cerr << "FilamentSceneBridge: editorGrid.filamat is empty; viewport grid disabled." << std::endl; + return; + } + + EditorGridMaterial_ = filament::Material::Builder() + .package(materialBuffer.data(), materialBuffer.size()) + .build(*Engine_); + if (EditorGridMaterial_ == nullptr) { + std::cerr << "FilamentSceneBridge: failed to build editor grid material." << std::endl; + return; + } + + EditorGridMaterialInstance_ = EditorGridMaterial_->createInstance(); + if (EditorGridMaterialInstance_ != nullptr) { + EditorGridMaterialInstance_->setParameter("minorAlpha", 0.045F); + EditorGridMaterialInstance_->setParameter("majorAlpha", 0.11F); + EditorGridMaterialInstance_->setParameter("axisAlpha", 0.18F); + EditorGridMaterialInstance_->setParameter("cameraPosition", filament::math::float3{0.0F, 0.0F, 0.0F}); + EditorGridMaterialInstance_->setParameter("fadeStart", 45.0F); + EditorGridMaterialInstance_->setParameter("fadeEnd", 220.0F); + } + + constexpr float extent = 1000.0F; + const MetaCoreEditorGridVertex vertices[] = { + {-extent, -extent, 0.0F}, + { extent, -extent, 0.0F}, + { extent, extent, 0.0F}, + {-extent, extent, 0.0F} + }; + const uint16_t indices[] = {0, 1, 2, 0, 2, 3}; + + EditorGridVertexBuffer_ = filament::VertexBuffer::Builder() + .vertexCount(4) + .bufferCount(1) + .attribute(filament::VertexAttribute::POSITION, 0, filament::VertexBuffer::AttributeType::FLOAT3, 0, sizeof(MetaCoreEditorGridVertex)) + .build(*Engine_); + EditorGridIndexBuffer_ = filament::IndexBuffer::Builder() + .indexCount(6) + .bufferType(filament::IndexBuffer::IndexType::USHORT) + .build(*Engine_); + + if (EditorGridVertexBuffer_ == nullptr || EditorGridIndexBuffer_ == nullptr || EditorGridMaterialInstance_ == nullptr) { + std::cerr << "FilamentSceneBridge: failed to allocate editor grid buffers." << std::endl; + DestroyEditorGridResources(); + return; + } + + auto* vertexCopy = new MetaCoreEditorGridVertex[4]; + std::copy(std::begin(vertices), std::end(vertices), vertexCopy); + EditorGridVertexBuffer_->setBufferAt( + *Engine_, + 0, + filament::VertexBuffer::BufferDescriptor( + vertexCopy, + sizeof(vertices), + [](void* buffer, size_t, void*) { delete[] static_cast(buffer); } + ) + ); + + auto* indexCopy = new uint16_t[6]; + std::copy(std::begin(indices), std::end(indices), indexCopy); + EditorGridIndexBuffer_->setBuffer( + *Engine_, + filament::IndexBuffer::BufferDescriptor( + indexCopy, + sizeof(indices), + [](void* buffer, size_t, void*) { delete[] static_cast(buffer); } + ) + ); + + EditorGridEntity_ = utils::EntityManager::get().create(); + filament::RenderableManager::Builder(1) + .boundingBox({{0.0F, 0.0F, 0.0F}, {extent, extent, 0.01F}}) + .geometry(0, filament::RenderableManager::PrimitiveType::TRIANGLES, EditorGridVertexBuffer_, EditorGridIndexBuffer_, 0, 6) + .material(0, EditorGridMaterialInstance_) + .culling(false) + .castShadows(false) + .receiveShadows(false) + .build(*Engine_, EditorGridEntity_); + + SyncEditorGridVisibility(); + } + + void DestroyEditorGridResources() { + if (Engine_ == nullptr) { + return; + } + + if (EditorGridEntity_) { + if (Scene_ != nullptr) { + Scene_->remove(EditorGridEntity_); + } + Engine_->destroy(EditorGridEntity_); + utils::EntityManager::get().destroy(EditorGridEntity_); + EditorGridEntity_.clear(); + } + if (EditorGridVertexBuffer_ != nullptr) { + Engine_->destroy(EditorGridVertexBuffer_); + EditorGridVertexBuffer_ = nullptr; + } + if (EditorGridIndexBuffer_ != nullptr) { + Engine_->destroy(EditorGridIndexBuffer_); + EditorGridIndexBuffer_ = nullptr; + } + if (EditorGridMaterialInstance_ != nullptr) { + Engine_->destroy(EditorGridMaterialInstance_); + EditorGridMaterialInstance_ = nullptr; + } + if (EditorGridMaterial_ != nullptr) { + Engine_->destroy(EditorGridMaterial_); + EditorGridMaterial_ = nullptr; + } + EditorGridInScene_ = false; + } + + void SyncEditorGridVisibility() { + if (Scene_ == nullptr || !EditorGridEntity_) { + EditorGridInScene_ = false; + return; + } + + if (EditorGridVisible_ && !EditorGridInScene_) { + Scene_->addEntity(EditorGridEntity_); + EditorGridInScene_ = true; + } else if (!EditorGridVisible_ && EditorGridInScene_) { + Scene_->remove(EditorGridEntity_); + EditorGridInScene_ = false; + } + } + + void UpdateEditorGridTransform(const MetaCoreSceneView& sceneView) { + if (Engine_ == nullptr || !EditorGridEntity_) { + return; + } + + auto& transformManager = Engine_->getTransformManager(); + auto instance = transformManager.getInstance(EditorGridEntity_); + if (!instance) { + return; + } + + const float x = std::floor(sceneView.CameraTarget.x / 10.0F) * 10.0F; + const float y = std::floor(sceneView.CameraTarget.y / 10.0F) * 10.0F; + const glm::mat4 transform = glm::translate(glm::mat4(1.0F), glm::vec3(x, y, 0.0F)); + transformManager.setTransform(instance, *reinterpret_cast(glm::value_ptr(transform))); + + if (EditorGridMaterialInstance_ != nullptr) { + EditorGridMaterialInstance_->setParameter( + "cameraPosition", + filament::math::float3{sceneView.CameraPosition.x, sceneView.CameraPosition.y, sceneView.CameraPosition.z} + ); + } + } + void ParseModelHierarchyToEcs(MetaCoreScene& scene, MetaCoreGameObject& parentObject, filament::gltfio::FilamentAsset* asset) { if (!asset) return; @@ -1192,6 +1413,7 @@ public: void ApplySceneView(const MetaCoreSceneView& sceneView) { if (!Camera_ || !View_) return; + UpdateEditorGridTransform(sceneView); // 1. 视图矩阵同步 const glm::mat4 viewMatrix = glm::lookAt(sceneView.CameraPosition, sceneView.CameraTarget, sceneView.CameraUp); @@ -1659,6 +1881,14 @@ private: std::unordered_map SceneLightEntities_; std::unordered_set EntitiesInScene_; + utils::Entity EditorGridEntity_{}; + filament::VertexBuffer* EditorGridVertexBuffer_ = nullptr; + filament::IndexBuffer* EditorGridIndexBuffer_ = nullptr; + filament::Material* EditorGridMaterial_ = nullptr; + filament::MaterialInstance* EditorGridMaterialInstance_ = nullptr; + bool EditorGridVisible_ = true; + bool EditorGridInScene_ = false; + filament::View* UIView_ = nullptr; MetaCoreImGuiHelper* ImGuiHelper_ = nullptr; filament::View* RuntimeUIView_ = nullptr; @@ -1708,6 +1938,10 @@ void MetaCoreFilamentSceneBridge::ApplySceneView(const MetaCoreSceneView& sceneV Impl_->ApplySceneView(sceneView); } +void MetaCoreFilamentSceneBridge::SetEditorGridVisible(bool visible) { + Impl_->SetEditorGridVisible(visible); +} + void MetaCoreFilamentSceneBridge::SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height) { Impl_->SetRuntimeUiOverlayFrame(frame, width, height); } diff --git a/Source/MetaCoreRender/Private/editorGrid.mat b/Source/MetaCoreRender/Private/editorGrid.mat new file mode 100644 index 0000000..be16ed3 --- /dev/null +++ b/Source/MetaCoreRender/Private/editorGrid.mat @@ -0,0 +1,83 @@ +material { + name : MetaCoreEditorGrid, + blending : transparent, + culling : none, + depthCulling : true, + depthWrite : false, + shadingModel : unlit, + variantFilter : [ skinning, shadowReceiver, vsm, fog ], + parameters : [ + { + type : float, + name : minorAlpha + }, + { + type : float, + name : majorAlpha + }, + { + type : float, + name : axisAlpha + }, + { + type : float3, + name : cameraPosition + }, + { + type : float, + name : fadeStart + }, + { + type : float, + name : fadeEnd + } + ], + variables : [ gridWorldPosition ] +} + +vertex { + void materialVertex(inout MaterialVertexInputs material) { + highp vec4 worldPosition = getWorldFromModelMatrix() * getPosition(); + material.gridWorldPosition.xyz = worldPosition.xyz; + } +} + +fragment { + highp float gridLine(highp vec2 coord, highp float pixelWidth) { + highp vec2 dudv = max(fwidth(coord), vec2(1e-6)); + highp vec2 uvMod = fract(coord); + highp vec2 uvDist = min(uvMod, 1.0 - uvMod); + highp vec2 distInPixels = uvDist / dudv; + highp vec2 lineAlpha = 1.0 - smoothstep(pixelWidth, pixelWidth + 0.85, distInPixels); + return max(lineAlpha.x, lineAlpha.y); + } + + highp float axisLine(highp float coord, highp float pixelWidth) { + highp float dudv = max(fwidth(coord), 1e-6); + highp float distInPixels = abs(coord) / dudv; + return 1.0 - smoothstep(pixelWidth, pixelWidth + 1.0, distInPixels); + } + + void material(inout MaterialInputs material) { + prepareMaterial(material); + + highp vec2 coord = variable_gridWorldPosition.xy; + + highp float distanceToCamera = distance(coord, materialParams.cameraPosition.xy); + highp float distanceFade = 1.0 - smoothstep(materialParams.fadeStart, materialParams.fadeEnd, distanceToCamera); + + highp vec2 minorDeriv = fwidth(coord); + highp float minorLodFade = 1.0 - smoothstep(0.10, 0.38, max(minorDeriv.x, minorDeriv.y)); + + highp float minor = gridLine(coord / 2.0, 0.18) * materialParams.minorAlpha * minorLodFade; + highp float major = gridLine(coord / 10.0, 0.32) * materialParams.majorAlpha; + highp float axis = max(axisLine(coord.x, 0.35), axisLine(coord.y, 0.35)) * materialParams.axisAlpha; + highp float alpha = max(max(minor, major), axis) * distanceFade; + + if (alpha < 0.004) { + discard; + } + + material.baseColor = vec4(vec3(0.46, 0.50, 0.56), alpha); + } +} diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h index 9ff85d9..e89a2e3 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreEditorViewportRenderer.h @@ -19,6 +19,7 @@ public: bool Initialize(MetaCoreRenderDevice& renderDevice, MetaCoreWindow& window, bool offscreen = true); void Shutdown(); void SetViewportRect(const MetaCoreViewportRect& viewportRect); + void SetEditorGridVisible(bool visible); void RenderSceneToViewport(MetaCoreScene& scene, const MetaCoreSceneView& sceneView, bool useScenePrimaryCamera = false); void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height); void RenderAll(); @@ -32,6 +33,7 @@ private: MetaCoreRenderDevice* RenderDevice_ = nullptr; MetaCoreFilamentSceneBridge FilamentSceneBridge_{}; MetaCoreViewportRect ViewportRect_{}; + bool EditorGridVisible_ = false; }; diff --git a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h index d1a731a..d8de6eb 100644 --- a/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h +++ b/Source/MetaCoreRender/Public/MetaCoreRender/MetaCoreFilamentSceneBridge.h @@ -32,6 +32,7 @@ public: void SyncScene(MetaCoreScene& scene, bool compatibilityMeshOnly = false, bool useScenePrimaryCamera = false); void SyncScene(const MetaCoreSceneRenderSyncSnapshot& snapshot, MetaCoreScene* scene = nullptr, bool compatibilityMeshOnly = false, bool useScenePrimaryCamera = false); void ApplySceneView(const MetaCoreSceneView& sceneView); + void SetEditorGridVisible(bool visible); void SetRuntimeUiOverlayFrame(const MetaCoreRuntimeUiFrame& frame, int width, int height); void RenderAll(); [[nodiscard]] uint32_t GetGLTextureId() const; diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 030af2b..0520568 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -277,6 +277,136 @@ void MetaCoreTestUndoRedo() { MetaCoreExpect(scene.GetGameObjects().size() == beforeCreateCount + 1, "Redo 后对象数量应再次增加"); } +void MetaCoreTestHierarchyEditingCommandsDirtyAndUndo() { + const std::filesystem::path tempProjectRoot = + std::filesystem::temp_directory_path() / "MetaCoreHierarchyEditingCommandsSmoke"; + std::filesystem::remove_all(tempProjectRoot); + std::filesystem::create_directories(tempProjectRoot / "Scenes"); + std::filesystem::create_directories(tempProjectRoot / "Assets"); + std::filesystem::create_directories(tempProjectRoot / "Library"); + + { + std::ofstream projectFile(tempProjectRoot / "MetaCore.project.json", std::ios::trunc); + MetaCoreExpect(projectFile.is_open(), "应能写入 Hierarchy 命令测试项目文件"); + projectFile << "{\n" + << " \"name\": \"HierarchyEditingCommandsSmoke\",\n" + << " \"version\": \"0.1.0\",\n" + << " \"scenes\": [],\n" + << " \"startup_scene\": \"\"\n" + << "}\n"; + } + + _putenv_s("METACORE_PROJECT_PATH", tempProjectRoot.string().c_str()); + + MetaCore::MetaCoreWindow window; + MetaCore::MetaCoreRenderDevice renderDevice; + MetaCore::MetaCoreEditorViewportRenderer viewportRenderer; + MetaCore::MetaCoreScene scene = MetaCore::MetaCoreCreateDefaultScene(); + MetaCore::MetaCoreLogService logService; + MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; + + auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); + coreServicesModule->Startup(moduleRegistry); + + MetaCore::MetaCoreEditorContext editorContext( + window, + renderDevice, + viewportRenderer, + scene, + logService, + moduleRegistry + ); + + const auto scenePersistenceService = moduleRegistry.ResolveService(); + const auto sceneEditingService = moduleRegistry.ResolveService(); + const auto clipboardService = moduleRegistry.ResolveService(); + MetaCoreExpect(scenePersistenceService != nullptr, "Hierarchy command test should resolve scene persistence service"); + MetaCoreExpect(sceneEditingService != nullptr, "Hierarchy command test should resolve scene editing service"); + MetaCoreExpect(clipboardService != nullptr, "Hierarchy command test should resolve clipboard service"); + + const std::filesystem::path scenePath = std::filesystem::path("Scenes") / "HierarchyCommands.mcscene.json"; + MetaCoreExpect(scenePersistenceService->SaveSceneAs(editorContext, scenePath), "Hierarchy command baseline scene should save"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Hierarchy command baseline should be clean after save"); + + const std::size_t baselineCount = scene.GetGameObjects().size(); + const auto createdId = sceneEditingService->CreateGameObject( + editorContext, + "HierarchyObject", + false, + std::optional{0} + ); + MetaCoreExpect(createdId.has_value(), "CreateGameObject command should create an object"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "CreateGameObject command should mark scene dirty"); + MetaCoreExpect(editorContext.UndoCommand(), "Undo CreateGameObject should succeed"); + MetaCoreExpect(scene.GetGameObjects().size() == baselineCount, "Undo CreateGameObject should restore object count"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo CreateGameObject should restore clean baseline"); + MetaCoreExpect(editorContext.RedoCommand(), "Redo CreateGameObject should succeed"); + MetaCoreExpect(scene.FindGameObject(*createdId), "Redo CreateGameObject should restore created object"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "Redo CreateGameObject should mark scene dirty again"); + + MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after create"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Hierarchy command scene should be clean after create save"); + + MetaCoreExpect(sceneEditingService->RenameGameObject(editorContext, *createdId, "HierarchyObjectRenamed"), "RenameGameObject command should rename object"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "RenameGameObject command should mark scene dirty"); + MetaCoreExpect(editorContext.UndoCommand(), "Undo RenameGameObject should succeed"); + MetaCoreExpect(scene.FindGameObject(*createdId).GetName() == "HierarchyObject", "Undo RenameGameObject should restore old name"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo RenameGameObject should restore clean baseline"); + MetaCoreExpect(editorContext.RedoCommand(), "Redo RenameGameObject should succeed"); + MetaCoreExpect(scene.FindGameObject(*createdId).GetName() == "HierarchyObjectRenamed", "Redo RenameGameObject should restore new name"); + + MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after rename"); + const auto parentId = sceneEditingService->CreateGameObject( + editorContext, + "HierarchyParent", + false, + std::optional{0} + ); + MetaCoreExpect(parentId.has_value(), "CreateGameObject command should create a parent object"); + MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after parent create"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Hierarchy command scene should be clean before reparent"); + + editorContext.SelectOnly(*createdId); + MetaCoreExpect(sceneEditingService->ReparentSelection(editorContext, *parentId), "ReparentSelection command should reparent selected object"); + MetaCoreExpect(scene.FindGameObject(*createdId).GetParentId() == *parentId, "ReparentSelection should set parent id"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "ReparentSelection command should mark scene dirty"); + MetaCoreExpect(editorContext.UndoCommand(), "Undo ReparentSelection should succeed"); + MetaCoreExpect(scene.FindGameObject(*createdId).GetParentId() == 0, "Undo ReparentSelection should restore root parent"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo ReparentSelection should restore clean baseline"); + MetaCoreExpect(editorContext.RedoCommand(), "Redo ReparentSelection should succeed"); + MetaCoreExpect(scene.FindGameObject(*createdId).GetParentId() == *parentId, "Redo ReparentSelection should restore parent id"); + + MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after reparent"); + editorContext.SelectOnly(*createdId); + const std::size_t countBeforeDuplicate = scene.GetGameObjects().size(); + MetaCoreExpect(clipboardService->DuplicateSelection(editorContext), "DuplicateSelection command should duplicate selected object"); + MetaCoreExpect(scene.GetGameObjects().size() > countBeforeDuplicate, "DuplicateSelection should add at least one object"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "DuplicateSelection command should mark scene dirty"); + MetaCoreExpect(editorContext.UndoCommand(), "Undo DuplicateSelection should succeed"); + MetaCoreExpect(scene.GetGameObjects().size() == countBeforeDuplicate, "Undo DuplicateSelection should restore object count"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo DuplicateSelection should restore clean baseline"); + MetaCoreExpect(editorContext.RedoCommand(), "Redo DuplicateSelection should succeed"); + MetaCoreExpect(scene.GetGameObjects().size() > countBeforeDuplicate, "Redo DuplicateSelection should add object again"); + + MetaCoreExpect(scenePersistenceService->SaveCurrentScene(editorContext), "Hierarchy command scene should save after duplicate"); + const MetaCore::MetaCoreId duplicatedId = editorContext.GetActiveObjectId(); + MetaCoreExpect(duplicatedId != 0 && duplicatedId != *createdId, "DuplicateSelection should select duplicated object"); + const std::size_t countBeforeDelete = scene.GetGameObjects().size(); + MetaCoreExpect(clipboardService->DeleteSelection(editorContext), "DeleteSelection command should delete selected object"); + MetaCoreExpect(scene.GetGameObjects().size() < countBeforeDelete, "DeleteSelection should remove selected object"); + MetaCoreExpect(scenePersistenceService->IsSceneDirty(), "DeleteSelection command should mark scene dirty"); + MetaCoreExpect(editorContext.UndoCommand(), "Undo DeleteSelection should succeed"); + MetaCoreExpect(scene.GetGameObjects().size() == countBeforeDelete, "Undo DeleteSelection should restore object count"); + MetaCoreExpect(!scenePersistenceService->IsSceneDirty(), "Undo DeleteSelection should restore clean baseline"); + MetaCoreExpect(editorContext.RedoCommand(), "Redo DeleteSelection should succeed"); + MetaCoreExpect(scene.GetGameObjects().size() < countBeforeDelete, "Redo DeleteSelection should remove object again"); + + coreServicesModule->Shutdown(moduleRegistry); + moduleRegistry.ShutdownServices(); + _putenv_s("METACORE_PROJECT_PATH", ""); + std::filesystem::remove_all(tempProjectRoot); +} + void MetaCoreTestBuiltinModuleComposition() { MetaCore::MetaCoreEditorModuleRegistry moduleRegistry; auto coreServicesModule = MetaCore::MetaCoreCreateBuiltinCoreServicesModule(); @@ -6358,6 +6488,8 @@ int main() { MetaCoreTestSelectionStateMachine(); std::cout << "[RUN] MetaCoreTestUndoRedo..." << std::endl; MetaCoreTestUndoRedo(); + std::cout << "[RUN] MetaCoreTestHierarchyEditingCommandsDirtyAndUndo..." << std::endl; + MetaCoreTestHierarchyEditingCommandsDirtyAndUndo(); std::cout << "[RUN] MetaCoreTestBuiltinModuleComposition..." << std::endl; MetaCoreTestBuiltinModuleComposition(); std::cout << "[RUN] MetaCoreTestLegacyBinarySceneStartupLoadCompatibility..." << std::endl;