diff --git a/CMakeLists.txt b/CMakeLists.txt index d1d2864..6236025 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -156,6 +156,7 @@ set(METACORE_EDITOR_HEADERS Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorCommandService.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorContext.h Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorModule.h + Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h ) set(METACORE_EDITOR_PRIVATE_HEADERS @@ -171,6 +172,7 @@ set(METACORE_EDITOR_SOURCES Source/MetaCoreEditor/Private/MetaCoreEditorCommandService.cpp Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp Source/MetaCoreEditor/Private/MetaCoreEditorModule.cpp + Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp third_party/ImGuizmo/ImGuizmo.cpp ) diff --git a/SandboxProject/Library/Layout/NullEditorShell_snapshot.txt b/SandboxProject/Library/Layout/NullEditorShell_snapshot.txt new file mode 100644 index 0000000..ba93eea --- /dev/null +++ b/SandboxProject/Library/Layout/NullEditorShell_snapshot.txt @@ -0,0 +1,10 @@ +Shell=NullEditorShell, Actions=11, Panels=8, Hierarchy=4, Assets=3, Inspector=UI Root, Console=3, Status=Focused selected object, Toolbar=on, RuntimePreview=on +Workspace[Default] + - Main Menu (main_menu) @ TopMenuBar visible=true + - Toolbar (toolbar) @ TopToolbar visible=true + - Hierarchy (hierarchy) @ LeftSidebar visible=true + - Project (project) @ LeftSidebar visible=true + - Viewport (viewport) @ CenterViewport visible=true + - Inspector (inspector) @ RightSidebar visible=true + - Console (console) @ BottomConsole visible=true + - Runtime Preview (runtime_preview) @ BottomRuntimePreview visible=true diff --git a/SandboxProject/Scenes/Main.mcscene.json b/SandboxProject/Scenes/Main.mcscene.json index 892c0bb..1028dce 100644 --- a/SandboxProject/Scenes/Main.mcscene.json +++ b/SandboxProject/Scenes/Main.mcscene.json @@ -103,6 +103,38 @@ 1 ] } + }, + { + "editor_metadata": { + "editor_only": false, + "locked": false, + "selected": false + }, + "id": "0000399eef6307dc90859f938da98af700000003", + "name": "UI Root", + "parent_id": "", + "transform": { + "position": [ + 0, + 0, + 0 + ], + "rotation": [ + 0, + 0, + 0 + ], + "scale": [ + 1, + 1, + 1 + ] + }, + "ui_document": { + "document_path": "Assets/UI/main_menu.rml", + "stylesheet_path": "Assets/UI/main_menu.rcss", + "visible": true + } } ] } \ No newline at end of file diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index f447330..95722cd 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -26,9 +26,7 @@ #include #include #include -#include #include -#include #include extern IMGUI_IMPL_API LRESULT ImGui_ImplWin32_WndProcHandler(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam); @@ -174,136 +172,6 @@ ImGuizmo::MODE MetaCoreToImGuizmoMode(MetaCoreGizmoMode mode) { return mode == MetaCoreGizmoMode::World ? ImGuizmo::WORLD : ImGuizmo::LOCAL; } -MetaCoreId MetaCorePickGameObjectFromViewport( - const MetaCoreScene& scene, - const MetaCoreSceneView& sceneView, - const MetaCoreSceneViewportState& viewportState, - const glm::vec2& cursorPosition -) { - if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { - return 0; - } - - const glm::vec2 localCursor( - cursorPosition.x - viewportState.Left, - cursorPosition.y - viewportState.Top - ); - - if (localCursor.x < 0.0F || localCursor.y < 0.0F || localCursor.x > viewportState.Width || localCursor.y > viewportState.Height) { - return 0; - } - - 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 float normalizedX = (localCursor.x / viewportState.Width) * 2.0F - 1.0F; - const float normalizedY = 1.0F - (localCursor.y / viewportState.Height) * 2.0F; - const glm::vec4 rayStartClip(normalizedX, normalizedY, -1.0F, 1.0F); - const glm::vec4 rayEndClip(normalizedX, normalizedY, 1.0F, 1.0F); - const glm::mat4 inverseViewProjection = glm::inverse(projectionMatrix * viewMatrix); - glm::vec4 rayStartWorld = inverseViewProjection * rayStartClip; - glm::vec4 rayEndWorld = inverseViewProjection * rayEndClip; - if (std::abs(rayStartWorld.w) < 0.0001F || std::abs(rayEndWorld.w) < 0.0001F) { - return 0; - } - - rayStartWorld /= rayStartWorld.w; - rayEndWorld /= rayEndWorld.w; - - const glm::vec3 rayOrigin(rayStartWorld); - const glm::vec3 rayDirection = glm::normalize(glm::vec3(rayEndWorld - rayStartWorld)); - - std::unordered_map worldMatrixCache; - const auto buildWorldMatrix = [&](const auto& self, MetaCoreId objectId) -> glm::mat4 { - if (objectId == 0) { - return glm::mat4(1.0F); - } - - if (const auto cacheIterator = worldMatrixCache.find(objectId); cacheIterator != worldMatrixCache.end()) { - return cacheIterator->second; - } - - const MetaCoreGameObject* object = scene.FindGameObject(objectId); - if (object == nullptr) { - return glm::mat4(1.0F); - } - - glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(object->Transform); - if (object->ParentId != 0) { - worldMatrix = self(self, object->ParentId) * worldMatrix; - } - - worldMatrixCache.emplace(objectId, worldMatrix); - return worldMatrix; - }; - - MetaCoreId bestObjectId = 0; - float bestHitDistance = std::numeric_limits::max(); - - for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) { - if (!gameObject.MeshRenderer.has_value() || !gameObject.MeshRenderer->Visible) { - continue; - } - - const glm::mat4 worldMatrix = buildWorldMatrix(buildWorldMatrix, gameObject.Id); - const glm::mat4 inverseWorldMatrix = glm::inverse(worldMatrix); - const glm::vec3 localRayOrigin = glm::vec3(inverseWorldMatrix * glm::vec4(rayOrigin, 1.0F)); - const glm::vec3 localRayDirection = glm::normalize(glm::vec3(inverseWorldMatrix * glm::vec4(rayDirection, 0.0F))); - - constexpr glm::vec3 localBoundsMin(-0.5F, -0.5F, -0.5F); - constexpr glm::vec3 localBoundsMax(0.5F, 0.5F, 0.5F); - - float nearDistance = 0.0F; - float farDistance = bestHitDistance; - bool hit = true; - - for (int axisIndex = 0; axisIndex < 3; ++axisIndex) { - const float rayAxisOrigin = localRayOrigin[axisIndex]; - const float rayAxisDirection = localRayDirection[axisIndex]; - - if (std::abs(rayAxisDirection) < 0.0001F) { - if (rayAxisOrigin < localBoundsMin[axisIndex] || rayAxisOrigin > localBoundsMax[axisIndex]) { - hit = false; - break; - } - continue; - } - - float axisNearDistance = (localBoundsMin[axisIndex] - rayAxisOrigin) / rayAxisDirection; - float axisFarDistance = (localBoundsMax[axisIndex] - rayAxisOrigin) / rayAxisDirection; - if (axisNearDistance > axisFarDistance) { - std::swap(axisNearDistance, axisFarDistance); - } - - nearDistance = std::max(nearDistance, axisNearDistance); - farDistance = std::min(farDistance, axisFarDistance); - if (nearDistance > farDistance) { - hit = false; - break; - } - } - - if (hit && farDistance >= 0.0F) { - const float hitDistance = nearDistance >= 0.0F ? nearDistance : farDistance; - if (hitDistance >= 0.0F && hitDistance < bestHitDistance) { - bestHitDistance = hitDistance; - bestObjectId = gameObject.Id; - } - } - } - - return bestObjectId; -} - } // namespace MetaCoreEditorApp::~MetaCoreEditorApp() { @@ -435,10 +303,6 @@ void MetaCoreEditorApp::ShutdownImGui() { } void MetaCoreEditorApp::DrawEditorFrame() { - static bool gizmoWasUsing = false; - static bool hasGizmoBeforeSnapshot = false; - static MetaCoreEditorStateSnapshot gizmoBeforeSnapshot{}; - const ImGuiViewport* mainViewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(mainViewport->WorkPos); ImGui::SetNextWindowSize(mainViewport->WorkSize); @@ -639,10 +503,7 @@ void MetaCoreEditorApp::DrawEditorFrame() { gizmoHovering = ImGuizmo::IsOver(); gizmoUsing = ImGuizmo::IsUsing(); - if (gizmoUsing && !gizmoWasUsing) { - gizmoBeforeSnapshot = EditorContext_->CaptureStateSnapshot(); - hasGizmoBeforeSnapshot = true; - } + SceneInteractionService_.HandleGizmoBeginUse(*EditorContext_, gizmoUsing); if (gizmoUsing) { // transformMatrix is in Panda space — apply directly. switch (EditorContext_->GetGizmoOperation()) { @@ -691,38 +552,17 @@ void MetaCoreEditorApp::DrawEditorFrame() { } if (viewportState.Hovered && !gizmoHovering && !gizmoUsing && !ImGui::GetIO().WantCaptureMouse && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { - const MetaCoreId pickedObjectId = MetaCorePickGameObjectFromViewport( + const MetaCoreId pickedObjectId = SceneInteractionService_.PickGameObjectFromViewport( Scene_, sceneView, viewportState, EditorContext_->GetInput().GetCursorPosition() ); - const bool ctrlDown = ImGui::GetIO().KeyCtrl; - const bool shiftDown = ImGui::GetIO().KeyShift; - if (pickedObjectId != 0) { - if (shiftDown) { - EditorContext_->SelectRangeByOrderedIds(Scene_.BuildHierarchyPreorder(), pickedObjectId, ctrlDown); - } else if (ctrlDown) { - EditorContext_->ToggleSelection(pickedObjectId); - } else { - EditorContext_->SelectOnly(pickedObjectId); - } - if (const MetaCoreGameObject* selectedObject = EditorContext_->GetSelectedGameObject(); selectedObject != nullptr) { - EditorContext_->AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "视口已选中对象: " + selectedObject->Name); - } - sceneView.SelectedObjectId = EditorContext_->GetActiveObjectId(); - } else if (!ctrlDown && !shiftDown) { - EditorContext_->ClearSelection(); - sceneView.SelectedObjectId = 0; - } + SceneInteractionService_.ApplyViewportSelection(*EditorContext_, Scene_, pickedObjectId); + sceneView.SelectedObjectId = EditorContext_->GetActiveObjectId(); } - if (!gizmoUsing && gizmoWasUsing && hasGizmoBeforeSnapshot) { - const MetaCoreEditorStateSnapshot gizmoAfterSnapshot = EditorContext_->CaptureStateSnapshot(); - (void)EditorContext_->CommitStateTransition("修改变换", gizmoBeforeSnapshot, gizmoAfterSnapshot, true); - hasGizmoBeforeSnapshot = false; - } - gizmoWasUsing = gizmoUsing; + SceneInteractionService_.HandleGizmoEndUse(*EditorContext_, gizmoUsing); ImGui::SetNextWindowPos(centralNode->Pos); ImGui::SetNextWindowSize(centralNode->Size); @@ -830,13 +670,11 @@ void MetaCoreEditorApp::DrawEditorFrame() { ImGui::PopStyleVar(3); } else { - gizmoWasUsing = false; - hasGizmoBeforeSnapshot = false; + SceneInteractionService_.ResetFrameState(); ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); } } else { - gizmoWasUsing = false; - hasGizmoBeforeSnapshot = false; + SceneInteractionService_.ResetFrameState(); ViewportRenderer_.SetViewportRect(MetaCoreViewportRect{}); } } diff --git a/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp new file mode 100644 index 0000000..c88f4f8 --- /dev/null +++ b/Source/MetaCoreEditor/Private/MetaCoreSceneInteractionService.cpp @@ -0,0 +1,207 @@ +#include "MetaCoreEditor/MetaCoreSceneInteractionService.h" + +#include "MetaCoreScene/MetaCoreScene.h" + +#include + +#define GLM_ENABLE_EXPERIMENTAL +#include +#include +#include + +#include +#include +#include + +namespace MetaCore { + +namespace { + +glm::mat4 MetaCoreBuildTransformMatrix(const MetaCoreTransformComponent& transform) { + const glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0F), transform.Position); + const glm::mat4 rotationMatrix = glm::yawPitchRoll( + glm::radians(transform.RotationEulerDegrees.y), + glm::radians(transform.RotationEulerDegrees.x), + glm::radians(transform.RotationEulerDegrees.z) + ); + const glm::mat4 scaleMatrix = glm::scale(glm::mat4(1.0F), transform.Scale); + return translationMatrix * rotationMatrix * scaleMatrix; +} + +} // namespace + +void MetaCoreSceneInteractionService::ResetFrameState() { + GizmoWasUsing_ = false; + HasGizmoBeforeSnapshot_ = false; +} + +MetaCoreId MetaCoreSceneInteractionService::PickGameObjectFromViewport( + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::vec2& cursorPosition +) const { + if (viewportState.Width <= 1.0F || viewportState.Height <= 1.0F) { + return 0; + } + + const glm::vec2 localCursor( + cursorPosition.x - viewportState.Left, + cursorPosition.y - viewportState.Top + ); + + if (localCursor.x < 0.0F || localCursor.y < 0.0F || localCursor.x > viewportState.Width || localCursor.y > viewportState.Height) { + return 0; + } + + 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 float normalizedX = (localCursor.x / viewportState.Width) * 2.0F - 1.0F; + const float normalizedY = 1.0F - (localCursor.y / viewportState.Height) * 2.0F; + const glm::vec4 rayStartClip(normalizedX, normalizedY, -1.0F, 1.0F); + const glm::vec4 rayEndClip(normalizedX, normalizedY, 1.0F, 1.0F); + const glm::mat4 inverseViewProjection = glm::inverse(projectionMatrix * viewMatrix); + glm::vec4 rayStartWorld = inverseViewProjection * rayStartClip; + glm::vec4 rayEndWorld = inverseViewProjection * rayEndClip; + if (std::abs(rayStartWorld.w) < 0.0001F || std::abs(rayEndWorld.w) < 0.0001F) { + return 0; + } + + rayStartWorld /= rayStartWorld.w; + rayEndWorld /= rayEndWorld.w; + + const glm::vec3 rayOrigin(rayStartWorld); + const glm::vec3 rayDirection = glm::normalize(glm::vec3(rayEndWorld - rayStartWorld)); + + std::unordered_map worldMatrixCache; + const auto buildWorldMatrix = [&](const auto& self, MetaCoreId objectId) -> glm::mat4 { + if (objectId == 0) { + return glm::mat4(1.0F); + } + + if (const auto cacheIterator = worldMatrixCache.find(objectId); cacheIterator != worldMatrixCache.end()) { + return cacheIterator->second; + } + + const MetaCoreGameObject* object = scene.FindGameObject(objectId); + if (object == nullptr) { + return glm::mat4(1.0F); + } + + glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(object->Transform); + if (object->ParentId != 0) { + worldMatrix = self(self, object->ParentId) * worldMatrix; + } + + worldMatrixCache.emplace(objectId, worldMatrix); + return worldMatrix; + }; + + MetaCoreId bestObjectId = 0; + float bestHitDistance = std::numeric_limits::max(); + + for (const MetaCoreGameObject& gameObject : scene.GetGameObjects()) { + if (!gameObject.MeshRenderer.has_value() || !gameObject.MeshRenderer->Visible) { + continue; + } + + const glm::mat4 worldMatrix = buildWorldMatrix(buildWorldMatrix, gameObject.Id); + const glm::mat4 inverseWorldMatrix = glm::inverse(worldMatrix); + const glm::vec3 localRayOrigin = glm::vec3(inverseWorldMatrix * glm::vec4(rayOrigin, 1.0F)); + const glm::vec3 localRayDirection = glm::normalize(glm::vec3(inverseWorldMatrix * glm::vec4(rayDirection, 0.0F))); + + constexpr glm::vec3 localBoundsMin(-0.5F, -0.5F, -0.5F); + constexpr glm::vec3 localBoundsMax(0.5F, 0.5F, 0.5F); + + float nearDistance = 0.0F; + float farDistance = bestHitDistance; + bool hit = true; + + for (int axisIndex = 0; axisIndex < 3; ++axisIndex) { + const float rayAxisOrigin = localRayOrigin[axisIndex]; + const float rayAxisDirection = localRayDirection[axisIndex]; + + if (std::abs(rayAxisDirection) < 0.0001F) { + if (rayAxisOrigin < localBoundsMin[axisIndex] || rayAxisOrigin > localBoundsMax[axisIndex]) { + hit = false; + break; + } + continue; + } + + float axisNearDistance = (localBoundsMin[axisIndex] - rayAxisOrigin) / rayAxisDirection; + float axisFarDistance = (localBoundsMax[axisIndex] - rayAxisOrigin) / rayAxisDirection; + if (axisNearDistance > axisFarDistance) { + std::swap(axisNearDistance, axisFarDistance); + } + + nearDistance = std::max(nearDistance, axisNearDistance); + farDistance = std::min(farDistance, axisFarDistance); + if (nearDistance > farDistance) { + hit = false; + break; + } + } + + if (hit && farDistance >= 0.0F) { + const float hitDistance = nearDistance >= 0.0F ? nearDistance : farDistance; + if (hitDistance >= 0.0F && hitDistance < bestHitDistance) { + bestHitDistance = hitDistance; + bestObjectId = gameObject.Id; + } + } + } + + return bestObjectId; +} + +void MetaCoreSceneInteractionService::ApplyViewportSelection( + MetaCoreEditorContext& editorContext, + const MetaCoreScene& scene, + MetaCoreId pickedObjectId +) const { + const bool ctrlDown = ImGui::GetIO().KeyCtrl; + const bool shiftDown = ImGui::GetIO().KeyShift; + if (pickedObjectId != 0) { + if (shiftDown) { + editorContext.SelectRangeByOrderedIds(scene.BuildHierarchyPreorder(), pickedObjectId, ctrlDown); + } else if (ctrlDown) { + editorContext.ToggleSelection(pickedObjectId); + } else { + editorContext.SelectOnly(pickedObjectId); + } + if (const MetaCoreGameObject* selectedObject = editorContext.GetSelectedGameObject(); selectedObject != nullptr) { + editorContext.AddConsoleMessage(MetaCoreLogLevel::Info, "Scene", "视口已选中对象: " + selectedObject->Name); + } + } else if (!ctrlDown && !shiftDown) { + editorContext.ClearSelection(); + } +} + +void MetaCoreSceneInteractionService::HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing) { + if (gizmoUsing && !GizmoWasUsing_) { + GizmoBeforeSnapshot_ = editorContext.CaptureStateSnapshot(); + HasGizmoBeforeSnapshot_ = true; + } +} + +void MetaCoreSceneInteractionService::HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing) { + if (!gizmoUsing && GizmoWasUsing_ && HasGizmoBeforeSnapshot_) { + const MetaCoreEditorStateSnapshot gizmoAfterSnapshot = editorContext.CaptureStateSnapshot(); + (void)editorContext.CommitStateTransition("修改变换", GizmoBeforeSnapshot_, gizmoAfterSnapshot, true); + HasGizmoBeforeSnapshot_ = false; + } + GizmoWasUsing_ = gizmoUsing; +} + +} // namespace MetaCore diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h index 1421a1d..62c2ed0 100644 --- a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreEditorApp.h @@ -2,6 +2,7 @@ #include "MetaCoreEditor/MetaCoreEditorContext.h" #include "MetaCoreEditor/MetaCoreEditorModule.h" +#include "MetaCoreEditor/MetaCoreSceneInteractionService.h" #include "MetaCoreFoundation/MetaCoreLogService.h" #include "MetaCorePlatform/MetaCoreWindow.h" @@ -38,6 +39,7 @@ private: MetaCoreScene Scene_{}; MetaCoreLogService LogService_{}; MetaCoreEditorModuleRegistry ModuleRegistry_{}; + MetaCoreSceneInteractionService SceneInteractionService_{}; std::vector> Modules_{}; std::unique_ptr EditorContext_{}; bool Initialized_ = false; diff --git a/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h new file mode 100644 index 0000000..5f12a19 --- /dev/null +++ b/Source/MetaCoreEditor/Public/MetaCoreEditor/MetaCoreSceneInteractionService.h @@ -0,0 +1,34 @@ +#pragma once + +#include "MetaCoreEditor/MetaCoreEditorContext.h" +#include "MetaCoreRender/MetaCoreRenderTypes.h" + +#include + +namespace MetaCore { + +class MetaCoreScene; + +// 管理 Scene 视口交互:拾取选择与 Gizmo 变换提交。 +class MetaCoreSceneInteractionService { +public: + void ResetFrameState(); + + [[nodiscard]] MetaCoreId PickGameObjectFromViewport( + const MetaCoreScene& scene, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::vec2& cursorPosition + ) const; + + void ApplyViewportSelection(MetaCoreEditorContext& editorContext, const MetaCoreScene& scene, MetaCoreId pickedObjectId) const; + void HandleGizmoBeginUse(MetaCoreEditorContext& editorContext, bool gizmoUsing); + void HandleGizmoEndUse(MetaCoreEditorContext& editorContext, bool gizmoUsing); + +private: + bool GizmoWasUsing_ = false; + bool HasGizmoBeforeSnapshot_ = false; + MetaCoreEditorStateSnapshot GizmoBeforeSnapshot_{}; +}; + +} // namespace MetaCore