diff --git a/SandboxProject/Assets/UI/main_menu.rml.mcasset b/SandboxProject/Assets/UI/main_menu.rml.mcasset index ea03c56..a0c619e 100644 Binary files a/SandboxProject/Assets/UI/main_menu.rml.mcasset and b/SandboxProject/Assets/UI/main_menu.rml.mcasset differ diff --git a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp index 53793d7..19a3518 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreBuiltinCoreServicesModule.cpp @@ -2850,6 +2850,7 @@ void MetaCoreDrawUiRendererComponentInspector(MetaCoreEditorContext& editorConte }; drawBoolField("可见", &MetaCoreUiRendererComponent::Visible); drawBoolField("接收输入", &MetaCoreUiRendererComponent::InputEnabled); + drawBoolField("始终朝向相机", &MetaCoreUiRendererComponent::FaceCamera); int layer = MetaCoreGetSharedSelectedValue( editorContext, diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp index d215876..200e813 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorApp.cpp @@ -5,6 +5,7 @@ #include "MetaCoreFoundation/MetaCorePackage.h" #include "MetaCoreFoundation/MetaCoreProject.h" #include "MetaCoreFoundation/MetaCoreReflection.h" +#include "MetaCoreScene/MetaCoreTransformUtils.h" #include "MetaCoreRender/MetaCoreSceneRenderSync.h" #include "MetaCoreEditorCameraController.h" @@ -28,6 +29,7 @@ #define GLM_ENABLE_EXPERIMENTAL #include +#include #include #include #include @@ -1103,6 +1105,88 @@ void MetaCoreDrawUiDocumentPreview( drawList.PopClipRect(); } +[[nodiscard]] bool MetaCoreProjectUiLocalPoint( + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::mat4& worldMatrix, + const glm::vec2& localPoint, + ImVec2& screenPoint +) { + const glm::vec3 worldPoint = glm::vec3(worldMatrix * glm::vec4(localPoint.x, localPoint.y, 0.0F, 1.0F)); + return MetaCoreProjectWorldPointToViewport(sceneView, viewportState, worldPoint, screenPoint); +} + +[[nodiscard]] bool MetaCoreProjectUiQuad( + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::mat4& worldMatrix, + const glm::vec2& minPoint, + const glm::vec2& maxPoint, + std::array& screenPoints +) { + return MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, glm::vec2(minPoint.x, minPoint.y), screenPoints[0]) && + MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, glm::vec2(maxPoint.x, minPoint.y), screenPoints[1]) && + MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, glm::vec2(maxPoint.x, maxPoint.y), screenPoints[2]) && + MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, glm::vec2(minPoint.x, maxPoint.y), screenPoints[3]); +} + +void MetaCoreDrawProjectedUiDocumentPreview( + ImDrawList& drawList, + const MetaCoreUiDocument& document, + const MetaCoreSceneView& sceneView, + const MetaCoreSceneViewportState& viewportState, + const glm::mat4& worldMatrix +) { + const float referenceWidth = static_cast(std::max(1, document.ReferenceWidth)); + const float referenceHeight = static_cast(std::max(1, document.ReferenceHeight)); + + for (const MetaCoreUiNodeDocument& node : document.Nodes) { + if (!node.Visible) { + continue; + } + + const float left = node.RectTransform.Position.x / referenceWidth - 0.5F; + const float right = (node.RectTransform.Position.x + node.RectTransform.Size.x) / referenceWidth - 0.5F; + const float top = 0.5F - node.RectTransform.Position.y / referenceHeight; + const float bottom = 0.5F - (node.RectTransform.Position.y + node.RectTransform.Size.y) / referenceHeight; + std::array points{}; + if (!MetaCoreProjectUiQuad(sceneView, viewportState, worldMatrix, glm::vec2(left, top), glm::vec2(right, bottom), points)) { + continue; + } + + const bool isText = node.Type == MetaCoreUiNodeType::Text; + const bool isButton = node.Type == MetaCoreUiNodeType::Button; + const bool isImage = node.Type == MetaCoreUiNodeType::Image; + if (!isText) { + drawList.AddConvexPolyFilled(points.data(), static_cast(points.size()), MetaCoreUiPreviewColor(node.Style.BackgroundColor, isButton ? 0.92F : 0.82F)); + } + drawList.AddPolyline( + points.data(), + static_cast(points.size()), + isButton ? IM_COL32(130, 190, 255, 220) : IM_COL32(132, 145, 166, isText ? 90 : 170), + ImDrawFlags_Closed, + isButton ? 1.4F : 1.0F + ); + + if (isImage) { + drawList.AddLine(points[0], points[2], IM_COL32(210, 220, 235, 115), 1.0F); + drawList.AddLine(points[1], points[3], IM_COL32(210, 220, 235, 115), 1.0F); + } + + const std::string label = !node.Text.empty() ? node.Text : (!node.Name.empty() ? node.Name : node.Id); + if (!label.empty()) { + ImVec2 textPoint{}; + const glm::vec2 textLocal( + left + std::max(3.0F, node.Style.Padding.x) / referenceWidth, + top - std::max(3.0F, node.Style.Padding.y) / referenceHeight + ); + if (MetaCoreProjectUiLocalPoint(sceneView, viewportState, worldMatrix, textLocal, textPoint)) { + drawList.AddText(textPoint, MetaCoreUiPreviewColor(node.Style.TextColor, 0.96F), label.c_str()); + } + } + } +} + void MetaCoreDrawScreenSpaceUiPreviewOverlay( const MetaCoreEditorContext& editorContext, const MetaCoreScene& scene, @@ -1222,6 +1306,42 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( } const MetaCoreTransformComponent& transform = gameObject.GetComponent(); + const bool selected = editorContext.GetSelectedObjectId() == gameObject.GetId(); + const ImU32 fillColor = selected ? IM_COL32(44, 136, 110, 48) : IM_COL32(44, 136, 110, 30); + const ImU32 strokeColor = selected ? IM_COL32(104, 224, 188, 230) : IM_COL32(104, 224, 188, 150); + const std::optional document = MetaCoreLoadUiPreviewDocument(editorContext, uiRenderer); + const std::string label = "3D UI: " + gameObject.GetName(); + + if (!uiRenderer.FaceCamera) { + const glm::mat4 worldMatrix = MetaCoreBuildTransformMatrix(transform); + std::array panelPoints{}; + if (!MetaCoreProjectUiQuad( + sceneView, + viewportState, + worldMatrix, + glm::vec2(-0.5F, 0.5F), + glm::vec2(0.5F, -0.5F), + panelPoints + )) { + continue; + } + + drawList->AddConvexPolyFilled(panelPoints.data(), static_cast(panelPoints.size()), fillColor); + drawList->AddPolyline(panelPoints.data(), static_cast(panelPoints.size()), strokeColor, ImDrawFlags_Closed, selected ? 2.0F : 1.2F); + if (document.has_value()) { + MetaCoreDrawProjectedUiDocumentPreview(*drawList, *document, sceneView, viewportState, worldMatrix); + } else { + drawList->AddText(ImVec2(panelPoints[0].x + 8.0F, panelPoints[0].y + 27.0F), IM_COL32(255, 196, 120, 230), "未能读取 UI 文档"); + } + drawList->AddText(ImVec2(panelPoints[0].x + 8.0F, panelPoints[0].y + 7.0F), strokeColor, label.c_str()); + + ImVec2 centerPoint{}; + if (MetaCoreProjectWorldPointToViewport(sceneView, viewportState, transform.Position, centerPoint)) { + drawList->AddCircleFilled(centerPoint, 3.0F, strokeColor, 12); + } + continue; + } + ImVec2 centerPoint{}; if (!MetaCoreProjectWorldPointToViewport(sceneView, viewportState, transform.Position, centerPoint)) { continue; @@ -1237,7 +1357,6 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( } pixelsPerWorldUnit = viewportState.Height / (2.0F * distanceAlongView * std::tan(fovRadians * 0.5F)); } - const float rawPreviewWidth = worldWidth * pixelsPerWorldUnit; const float rawPreviewHeight = worldHeight * pixelsPerWorldUnit; const float maxPreviewWidth = std::max(64.0F, viewportState.Width * 0.9F); @@ -1247,21 +1366,16 @@ void MetaCoreDrawWorldSpaceUiPreviewOverlay( const float previewHeight = std::max(24.0F, rawPreviewHeight * fitScale); const ImVec2 rectMin(centerPoint.x - previewWidth * 0.5F, centerPoint.y - previewHeight * 0.5F); const ImVec2 rectMax(rectMin.x + previewWidth, rectMin.y + previewHeight); - const bool selected = editorContext.GetSelectedObjectId() == gameObject.GetId(); - const ImU32 fillColor = selected ? IM_COL32(44, 136, 110, 48) : IM_COL32(44, 136, 110, 30); - const ImU32 strokeColor = selected ? IM_COL32(104, 224, 188, 230) : IM_COL32(104, 224, 188, 150); drawList->AddRectFilled(rectMin, rectMax, fillColor, 4.0F); drawList->AddRect(rectMin, rectMax, strokeColor, 4.0F, 0, selected ? 2.0F : 1.2F); - const std::optional document = MetaCoreLoadUiPreviewDocument(editorContext, uiRenderer); if (document.has_value()) { MetaCoreDrawUiDocumentPreview(*drawList, *document, rectMin, ImVec2(previewWidth, previewHeight)); } else { drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 27.0F), IM_COL32(255, 196, 120, 230), "未能读取 UI 文档"); } - const std::string label = "3D UI: " + gameObject.GetName(); drawList->AddText(ImVec2(rectMin.x + 8.0F, rectMin.y + 7.0F), strokeColor, label.c_str()); drawList->AddCircleFilled(centerPoint, 3.0F, strokeColor, 12); } diff --git a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp index f8e5302..8b96e3a 100644 --- a/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp +++ b/Source/MetaCoreEditor/Private/MetaCoreEditorContext.cpp @@ -179,6 +179,7 @@ private: lhs.UiRenderer->RenderMode != rhs.UiRenderer->RenderMode || lhs.UiRenderer->Visible != rhs.UiRenderer->Visible || lhs.UiRenderer->InputEnabled != rhs.UiRenderer->InputEnabled || + lhs.UiRenderer->FaceCamera != rhs.UiRenderer->FaceCamera || lhs.UiRenderer->Layer != rhs.UiRenderer->Layer || !MetaCoreNearlyEqual(lhs.UiRenderer->PixelsPerUnit, rhs.UiRenderer->PixelsPerUnit) || !MetaCoreNearlyEqualVec3(lhs.UiRenderer->WorldSize, rhs.UiRenderer->WorldSize)) { diff --git a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp index 8555079..48c1a20 100644 --- a/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp +++ b/Source/MetaCoreScene/Private/MetaCoreSceneSerializer.cpp @@ -169,6 +169,7 @@ static json GameObjectDataToJson(const MetaCoreGameObjectData& obj, const MetaCo else if (uiField.Name == "RenderMode") uiJson["RenderMode"] = static_cast(ui.RenderMode); else if (uiField.Name == "Visible") uiJson["Visible"] = ui.Visible; else if (uiField.Name == "InputEnabled") uiJson["InputEnabled"] = ui.InputEnabled; + else if (uiField.Name == "FaceCamera") uiJson["FaceCamera"] = ui.FaceCamera; else if (uiField.Name == "Layer") uiJson["Layer"] = ui.Layer; else if (uiField.Name == "PixelsPerUnit") uiJson["PixelsPerUnit"] = ui.PixelsPerUnit; else if (uiField.Name == "WorldSize") uiJson["WorldSize"] = Vec3ToJson(ui.WorldSize); @@ -337,6 +338,7 @@ static MetaCoreGameObjectData JsonToGameObjectData(const json& objJson, const Me else if (uiField.Name == "RenderMode" && uiJson.contains("RenderMode")) ui.RenderMode = static_cast(uiJson["RenderMode"].get()); else if (uiField.Name == "Visible" && uiJson.contains("Visible")) ui.Visible = uiJson["Visible"].get(); else if (uiField.Name == "InputEnabled" && uiJson.contains("InputEnabled")) ui.InputEnabled = uiJson["InputEnabled"].get(); + else if (uiField.Name == "FaceCamera" && uiJson.contains("FaceCamera")) ui.FaceCamera = uiJson["FaceCamera"].get(); else if (uiField.Name == "Layer" && uiJson.contains("Layer")) ui.Layer = uiJson["Layer"].get(); else if (uiField.Name == "PixelsPerUnit" && uiJson.contains("PixelsPerUnit")) ui.PixelsPerUnit = uiJson["PixelsPerUnit"].get(); else if (uiField.Name == "WorldSize" && uiJson.contains("WorldSize")) ui.WorldSize = JsonToVec3(uiJson["WorldSize"]); diff --git a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h index 4e80772..ff53a9b 100644 --- a/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h +++ b/Source/MetaCoreScene/Public/MetaCoreScene/MetaCoreComponents.h @@ -211,6 +211,8 @@ struct MetaCoreUiRendererComponent { MC_PROPERTY() bool InputEnabled = true; MC_PROPERTY() + bool FaceCamera = false; + MC_PROPERTY() std::int32_t Layer = 0; MC_PROPERTY() float PixelsPerUnit = 1000.0F; diff --git a/tests/MetaCoreSmokeTests.cpp b/tests/MetaCoreSmokeTests.cpp index 8dbf62f..a94ccd7 100644 --- a/tests/MetaCoreSmokeTests.cpp +++ b/tests/MetaCoreSmokeTests.cpp @@ -4090,6 +4090,7 @@ void MetaCoreTestUiRendererComponentSnapshotAndJson() { uiRenderer.RenderMode = MetaCore::MetaCoreUiRenderMode::WorldSpace; uiRenderer.Visible = false; uiRenderer.InputEnabled = false; + uiRenderer.FaceCamera = true; uiRenderer.Layer = 42; uiRenderer.PixelsPerUnit = 512.0F; uiRenderer.WorldSize = glm::vec3(3.0F, 2.0F, 0.0F); @@ -4105,6 +4106,7 @@ void MetaCoreTestUiRendererComponentSnapshotAndJson() { const auto& restoredRenderer = restoredObject.GetComponent(); MetaCoreExpect(restoredRenderer.UiDocumentAssetGuid == uiGuid, "恢复快照后 UI 资产 Guid 应一致"); MetaCoreExpect(restoredRenderer.RenderMode == MetaCore::MetaCoreUiRenderMode::WorldSpace, "恢复快照后渲染模式应一致"); + MetaCoreExpect(restoredRenderer.FaceCamera, "恢复快照后朝向相机设置应一致"); MetaCoreExpect(restoredRenderer.Layer == 42, "恢复快照后层级应一致"); MetaCoreExpectVec3Near(restoredRenderer.WorldSize, glm::vec3(3.0F, 2.0F, 0.0F), "恢复快照后世界尺寸应一致"); @@ -4133,6 +4135,7 @@ void MetaCoreTestUiRendererComponentSnapshotAndJson() { MetaCoreExpect(loadedRenderer.RenderMode == MetaCore::MetaCoreUiRenderMode::WorldSpace, "JSON 往返后渲染模式应一致"); MetaCoreExpect(!loadedRenderer.Visible, "JSON 往返后可见状态应一致"); MetaCoreExpect(!loadedRenderer.InputEnabled, "JSON 往返后输入状态应一致"); + MetaCoreExpect(loadedRenderer.FaceCamera, "JSON 往返后朝向相机设置应一致"); MetaCoreExpect(loadedRenderer.Layer == 42, "JSON 往返后层级应一致"); MetaCoreExpect(std::abs(loadedRenderer.PixelsPerUnit - 512.0F) <= 0.0001F, "JSON 往返后 PPU 应一致"); MetaCoreExpectVec3Near(loadedRenderer.WorldSize, glm::vec3(3.0F, 2.0F, 0.0F), "JSON 往返后世界尺寸应一致");