Refactor scene viewport orientation gizmo

This commit is contained in:
2026-03-30 00:48:15 +08:00
parent c8f79dfb0f
commit 7aca8199be
4 changed files with 483 additions and 61 deletions

View File

@@ -0,0 +1,456 @@
#include "SceneViewportOrientationGizmo.h"
#include <XCEngine/Core/Math/Color.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <vector>
namespace XCEngine {
namespace Editor {
namespace {
constexpr float kViewerDistance = 112.0f;
constexpr float kWidgetInset = 70.0f;
constexpr float kCubeHalfExtent = 8.5f;
constexpr float kPositiveAxisLength = 31.5f;
constexpr float kNegativeAxisLength = kPositiveAxisLength;
constexpr int kConeCapSegments = 20;
constexpr float kTau = 6.28318530718f;
ImU32 ToImGuiColor(const Math::Color& color) {
const auto toChannel = [](float value) -> int {
return static_cast<int>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
};
return IM_COL32(
toChannel(color.r),
toChannel(color.g),
toChannel(color.b),
toChannel(color.a));
}
float Saturate(float value) {
return std::clamp(value, 0.0f, 1.0f);
}
Math::Color LerpColor(const Math::Color& a, const Math::Color& b, float t) {
const float clamped = Saturate(t);
return Math::Color(
a.r + (b.r - a.r) * clamped,
a.g + (b.g - a.g) * clamped,
a.b + (b.b - a.b) * clamped,
a.a + (b.a - a.a) * clamped);
}
Math::Color MultiplyColor(const Math::Color& color, float factor) {
return Math::Color(
Saturate(color.r * factor),
Saturate(color.g * factor),
Saturate(color.b * factor),
color.a);
}
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
}
ImVec2 NormalizeImVec2(const ImVec2& value) {
const float lengthSq = value.x * value.x + value.y * value.y;
if (lengthSq <= 1e-6f) {
return ImVec2(0.0f, -1.0f);
}
const float invLength = 1.0f / std::sqrt(lengthSq);
return ImVec2(value.x * invLength, value.y * invLength);
}
ImVec2 LerpImVec2(const ImVec2& a, const ImVec2& b, float t) {
return ImVec2(
a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t);
}
void BuildPerpendicularBasis(
const Math::Vector3& axis,
Math::Vector3& outTangent,
Math::Vector3& outBitangent) {
const Math::Vector3 normalizedAxis = NormalizeVector3(axis, Math::Vector3::Forward());
Math::Vector3 reference = std::abs(normalizedAxis.y) < 0.85f
? Math::Vector3::Up()
: Math::Vector3::Right();
if (std::abs(Math::Vector3::Dot(normalizedAxis, reference)) > 0.95f) {
reference = Math::Vector3::Forward();
}
outTangent = Math::Vector3::Cross(normalizedAxis, reference);
if (outTangent.SqrMagnitude() <= Math::EPSILON) {
outTangent = Math::Vector3::Right();
} else {
outTangent = outTangent.Normalized();
}
outBitangent = Math::Vector3::Cross(outTangent, normalizedAxis);
if (outBitangent.SqrMagnitude() <= Math::EPSILON) {
outBitangent = Math::Vector3::Up();
} else {
outBitangent = outBitangent.Normalized();
}
}
Math::Vector3 TransformToCameraSpace(
const SceneViewportOverlayData& overlay,
const Math::Vector3& localPoint) {
const Math::Vector3 cameraRight = NormalizeVector3(overlay.cameraRight, Math::Vector3::Right());
const Math::Vector3 cameraUp = NormalizeVector3(overlay.cameraUp, Math::Vector3::Up());
const Math::Vector3 cameraForward = NormalizeVector3(overlay.cameraForward, Math::Vector3::Forward());
return Math::Vector3(
Math::Vector3::Dot(localPoint, cameraRight),
Math::Vector3::Dot(localPoint, cameraUp),
Math::Vector3::Dot(localPoint, cameraForward));
}
struct ProjectedPoint {
ImVec2 position = ImVec2(0.0f, 0.0f);
float scale = 1.0f;
float depth = 0.0f;
};
ProjectedPoint ProjectPoint(const ImVec2& center, const Math::Vector3& point) {
const float scale = kViewerDistance / std::max(34.0f, kViewerDistance + point.z);
return {
ImVec2(center.x + point.x * scale, center.y - point.y * scale),
scale,
point.z
};
}
struct AxisHandleVisual {
Math::Vector3 cameraDirection = Math::Vector3::Zero();
Math::Color baseColor = Math::Color::White();
const char* label = nullptr;
bool positive = false;
float sortDepth = 0.0f;
};
struct CubeFaceVisual {
std::array<ImVec2, 4> points = {};
Math::Vector3 cameraNormal = Math::Vector3::Zero();
float averageDepth = 0.0f;
};
float ComputePolygonSignedArea(const ImVec2* points, int pointCount) {
if (points == nullptr || pointCount < 3) {
return 0.0f;
}
float area = 0.0f;
for (int i = 0; i < pointCount; ++i) {
const ImVec2& a = points[i];
const ImVec2& b = points[(i + 1) % pointCount];
area += a.x * b.y - b.x * a.y;
}
return area * 0.5f;
}
void DrawAxisLabel(ImDrawList* drawList, const ImVec2& position, const char* label) {
if (drawList == nullptr || label == nullptr) {
return;
}
ImVec2 textPosition = position;
const ImVec2 labelSize = ImGui::CalcTextSize(label);
textPosition.x -= labelSize.x * 0.5f;
textPosition.y -= labelSize.y * 0.5f;
drawList->AddText(
ImVec2(textPosition.x + 1.0f, textPosition.y + 1.0f),
IM_COL32(0, 0, 0, 120),
label);
drawList->AddText(textPosition, IM_COL32(245, 245, 245, 255), label);
}
void DrawCenterCube(
ImDrawList* drawList,
const SceneViewportOverlayData& overlay,
const ImVec2& center) {
if (drawList == nullptr) {
return;
}
const std::array<Math::Vector3, 8> cubeVertices = {{
Math::Vector3(-kCubeHalfExtent, -kCubeHalfExtent, -kCubeHalfExtent),
Math::Vector3(kCubeHalfExtent, -kCubeHalfExtent, -kCubeHalfExtent),
Math::Vector3(kCubeHalfExtent, kCubeHalfExtent, -kCubeHalfExtent),
Math::Vector3(-kCubeHalfExtent, kCubeHalfExtent, -kCubeHalfExtent),
Math::Vector3(-kCubeHalfExtent, -kCubeHalfExtent, kCubeHalfExtent),
Math::Vector3(kCubeHalfExtent, -kCubeHalfExtent, kCubeHalfExtent),
Math::Vector3(kCubeHalfExtent, kCubeHalfExtent, kCubeHalfExtent),
Math::Vector3(-kCubeHalfExtent, kCubeHalfExtent, kCubeHalfExtent)
}};
struct FaceDefinition {
std::array<int, 4> indices;
Math::Vector3 normal;
};
const std::array<FaceDefinition, 6> faces = {{
{ { 1, 5, 6, 2 }, Math::Vector3::Right() },
{ { 4, 0, 3, 7 }, Math::Vector3::Left() },
{ { 3, 2, 6, 7 }, Math::Vector3::Up() },
{ { 0, 4, 5, 1 }, Math::Vector3::Down() },
{ { 4, 5, 6, 7 }, Math::Vector3::Forward() },
{ { 0, 1, 2, 3 }, Math::Vector3::Back() }
}};
std::array<ProjectedPoint, 8> projectedVertices = {};
for (size_t i = 0; i < cubeVertices.size(); ++i) {
projectedVertices[i] = ProjectPoint(center, TransformToCameraSpace(overlay, cubeVertices[i]));
}
std::vector<CubeFaceVisual> visibleFaces;
visibleFaces.reserve(3);
for (const FaceDefinition& face : faces) {
const Math::Vector3 cameraNormal = TransformToCameraSpace(overlay, face.normal);
if (cameraNormal.z >= -0.02f) {
continue;
}
CubeFaceVisual visual = {};
visual.cameraNormal = cameraNormal;
for (size_t i = 0; i < face.indices.size(); ++i) {
const ProjectedPoint& projected = projectedVertices[face.indices[i]];
visual.points[i] = projected.position;
visual.averageDepth += projected.depth;
}
visual.averageDepth /= 4.0f;
visibleFaces.push_back(visual);
}
std::sort(
visibleFaces.begin(),
visibleFaces.end(),
[](const CubeFaceVisual& lhs, const CubeFaceVisual& rhs) {
return lhs.averageDepth > rhs.averageDepth;
});
const Math::Vector3 lightDirection = NormalizeVector3(
Math::Vector3(-0.35f, 0.80f, -0.90f),
Math::Vector3(-0.35f, 0.80f, -0.90f));
for (const CubeFaceVisual& face : visibleFaces) {
const float lightFactor = std::max(0.0f, Math::Vector3::Dot(face.cameraNormal.Normalized(), lightDirection));
const Math::Color faceColor = LerpColor(
Math::Color(0.42f, 0.44f, 0.47f, 1.0f),
Math::Color(0.72f, 0.74f, 0.78f, 1.0f),
0.22f + lightFactor * 0.78f);
const float projectedArea = std::abs(ComputePolygonSignedArea(face.points.data(), 4));
if (projectedArea <= 5.0f) {
for (int i = 0; i < 4; ++i) {
const ImVec2& a = face.points[i];
const ImVec2& b = face.points[(i + 1) % 4];
drawList->AddLine(
ImVec2(a.x + 1.0f, a.y + 1.2f),
ImVec2(b.x + 1.0f, b.y + 1.2f),
IM_COL32(0, 0, 0, 42),
1.6f);
drawList->AddLine(a, b, ToImGuiColor(faceColor), 1.2f);
}
} else {
const ImVec2 shadowPoints[] = {
ImVec2(face.points[0].x + 1.5f, face.points[0].y + 1.7f),
ImVec2(face.points[1].x + 1.5f, face.points[1].y + 1.7f),
ImVec2(face.points[2].x + 1.5f, face.points[2].y + 1.7f),
ImVec2(face.points[3].x + 1.5f, face.points[3].y + 1.7f)
};
drawList->AddConvexPolyFilled(shadowPoints, 4, IM_COL32(0, 0, 0, 52));
drawList->AddConvexPolyFilled(face.points.data(), 4, ToImGuiColor(faceColor));
drawList->AddPolyline(face.points.data(), 4, IM_COL32(255, 255, 255, 44), true, 1.0f);
}
}
}
void DrawAxisHandle(
ImDrawList* drawList,
const ImVec2& center,
const AxisHandleVisual& handle) {
if (drawList == nullptr || handle.cameraDirection.SqrMagnitude() <= Math::EPSILON) {
return;
}
const float frontFactor = Saturate((-handle.cameraDirection.z + 1.0f) * 0.5f);
const float tipDistance = kCubeHalfExtent + 0.65f;
const float capDistance = handle.positive ? kPositiveAxisLength : kNegativeAxisLength;
const float capRadius = 7.1f;
const Math::Vector3 tipPoint3 = handle.cameraDirection * tipDistance;
const Math::Vector3 capCenter3 = handle.cameraDirection * capDistance;
const ProjectedPoint tip = ProjectPoint(center, tipPoint3);
const ProjectedPoint capCenter = ProjectPoint(center, capCenter3);
const ImVec2 axisVector(
capCenter.position.x - tip.position.x,
capCenter.position.y - tip.position.y);
const float axisLengthSq = axisVector.x * axisVector.x + axisVector.y * axisVector.y;
const float axisLength = std::sqrt(axisLengthSq);
const ImVec2 axisDirection = axisLength > 1e-4f
? ImVec2(axisVector.x / axisLength, axisVector.y / axisLength)
: ImVec2(0.0f, 0.0f);
const ImVec2 sideDirection(-axisDirection.y, axisDirection.x);
Math::Vector3 capTangent = Math::Vector3::Right();
Math::Vector3 capBitangent = Math::Vector3::Up();
BuildPerpendicularBasis(handle.cameraDirection, capTangent, capBitangent);
std::array<ImVec2, kConeCapSegments> capPoints = {};
ImVec2 leftPoint = capCenter.position;
ImVec2 rightPoint = capCenter.position;
float maxSide = std::numeric_limits<float>::lowest();
float minSide = std::numeric_limits<float>::max();
float maxAxis = std::numeric_limits<float>::lowest();
float minAxis = std::numeric_limits<float>::max();
for (int i = 0; i < kConeCapSegments; ++i) {
const float angle = (static_cast<float>(i) / static_cast<float>(kConeCapSegments)) * kTau;
const Math::Vector3 ringPoint =
capCenter3 +
capTangent * (std::cos(angle) * capRadius) +
capBitangent * (std::sin(angle) * capRadius);
capPoints[i] = ProjectPoint(center, ringPoint).position;
const ImVec2 offset(
capPoints[i].x - capCenter.position.x,
capPoints[i].y - capCenter.position.y);
const float sideValue = offset.x * sideDirection.x + offset.y * sideDirection.y;
const float axisValue = offset.x * axisDirection.x + offset.y * axisDirection.y;
if (sideValue > maxSide) {
maxSide = sideValue;
leftPoint = capPoints[i];
}
if (sideValue < minSide) {
minSide = sideValue;
rightPoint = capPoints[i];
}
maxAxis = std::max(maxAxis, axisValue);
minAxis = std::min(minAxis, axisValue);
}
const Math::Color bodyColor = handle.positive
? LerpColor(MultiplyColor(handle.baseColor, 0.82f), Math::Color(0.98f, 0.98f, 0.98f, 1.0f), frontFactor * 0.16f)
: LerpColor(Math::Color(0.66f, 0.66f, 0.66f, 1.0f), Math::Color(0.95f, 0.95f, 0.95f, 1.0f), frontFactor);
const Math::Color lightColor = LerpColor(bodyColor, Math::Color::White(), handle.positive ? 0.14f : 0.20f);
const Math::Color darkColor = MultiplyColor(bodyColor, handle.positive ? 0.76f : 0.86f);
const Math::Color capColor = handle.positive
? LerpColor(bodyColor, Math::Color::White(), 0.08f + frontFactor * 0.12f)
: LerpColor(bodyColor, Math::Color::White(), 0.16f + frontFactor * 0.14f);
const ImVec2 shadowTriangle[] = {
ImVec2(tip.position.x + 1.2f, tip.position.y + 1.4f),
ImVec2(leftPoint.x + 1.2f, leftPoint.y + 1.4f),
ImVec2(rightPoint.x + 1.2f, rightPoint.y + 1.4f)
};
const ImVec2 leftFacet[] = {
tip.position,
leftPoint,
capCenter.position
};
const ImVec2 rightFacet[] = {
tip.position,
capCenter.position,
rightPoint
};
drawList->AddConvexPolyFilled(shadowTriangle, 3, IM_COL32(0, 0, 0, 58));
drawList->AddConvexPolyFilled(leftFacet, 3, ToImGuiColor(lightColor));
drawList->AddConvexPolyFilled(rightFacet, 3, ToImGuiColor(darkColor));
drawList->AddLine(tip.position, leftPoint, IM_COL32(255, 255, 255, handle.positive ? 34 : 44), 1.0f);
drawList->AddLine(tip.position, rightPoint, IM_COL32(0, 0, 0, 38), 1.0f);
const float capMinorSpan = maxAxis - minAxis;
if (capMinorSpan <= 1.35f) {
drawList->AddLine(
ImVec2(rightPoint.x + 1.0f, rightPoint.y + 1.2f),
ImVec2(leftPoint.x + 1.0f, leftPoint.y + 1.2f),
IM_COL32(0, 0, 0, 52),
2.4f);
drawList->AddLine(
rightPoint,
leftPoint,
ToImGuiColor(capColor),
2.0f);
drawList->AddLine(
rightPoint,
leftPoint,
IM_COL32(255, 255, 255, handle.positive ? 56 : 68),
1.0f);
} else {
drawList->AddConvexPolyFilled(capPoints.data(), static_cast<int>(capPoints.size()), ToImGuiColor(capColor));
drawList->AddPolyline(
capPoints.data(),
static_cast<int>(capPoints.size()),
IM_COL32(255, 255, 255, handle.positive ? 60 : 72),
true,
1.0f);
}
if (handle.positive && handle.label != nullptr) {
const float labelOffset = 9.0f * Saturate((axisLength - 2.0f) / 12.0f);
const ImVec2 labelPosition(
capCenter.position.x + axisDirection.x * labelOffset,
capCenter.position.y + axisDirection.y * labelOffset);
DrawAxisLabel(drawList, labelPosition, handle.label);
}
}
} // namespace
void DrawSceneViewportOrientationGizmo(
ImDrawList* drawList,
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax) {
if (drawList == nullptr || !overlay.valid) {
return;
}
const ImVec2 center(viewportMax.x - kWidgetInset, viewportMin.y + kWidgetInset);
const std::array<AxisHandleVisual, 6> handles = {{
{ TransformToCameraSpace(overlay, Math::Vector3::Right()), Math::Color(0.91f, 0.09f, 0.05f, 1.0f), "x", true, 0.0f },
{ TransformToCameraSpace(overlay, Math::Vector3::Left()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
{ TransformToCameraSpace(overlay, Math::Vector3::Up()), Math::Color(0.45f, 1.0f, 0.12f, 1.0f), "y", true, 0.0f },
{ TransformToCameraSpace(overlay, Math::Vector3::Down()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
{ TransformToCameraSpace(overlay, Math::Vector3::Forward()), Math::Color(0.11f, 0.29f, 1.0f, 1.0f), "z", true, 0.0f },
{ TransformToCameraSpace(overlay, Math::Vector3::Back()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f }
}};
std::vector<AxisHandleVisual> sortedHandles(handles.begin(), handles.end());
for (AxisHandleVisual& handle : sortedHandles) {
handle.sortDepth = handle.cameraDirection.z;
}
std::sort(
sortedHandles.begin(),
sortedHandles.end(),
[](const AxisHandleVisual& lhs, const AxisHandleVisual& rhs) {
return lhs.sortDepth > rhs.sortDepth;
});
for (const AxisHandleVisual& handle : sortedHandles) {
if (handle.sortDepth > 0.0f) {
DrawAxisHandle(drawList, center, handle);
}
}
DrawCenterCube(drawList, overlay, center);
for (const AxisHandleVisual& handle : sortedHandles) {
if (handle.sortDepth <= 0.0f) {
DrawAxisHandle(drawList, center, handle);
}
}
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -0,0 +1,17 @@
#pragma once
#include "IViewportHostService.h"
#include <imgui.h>
namespace XCEngine {
namespace Editor {
void DrawSceneViewportOrientationGizmo(
ImDrawList* drawList,
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax);
} // namespace Editor
} // namespace XCEngine

View File

@@ -1,15 +1,13 @@
#include "SceneViewportOverlayRenderer.h"
#include "SceneViewportMath.h"
#include "SceneViewportOrientationGizmo.h"
#include <algorithm>
namespace XCEngine {
namespace Editor {
namespace {
Math::Matrix4x4 BuildOverlayViewMatrix(const SceneViewportOverlayData& overlay) {
return BuildSceneViewportViewMatrix(overlay);
}
ImU32 ToImGuiColor(const Math::Color& color) {
const auto toChannel = [](float value) -> int {
return static_cast<int>(std::clamp(value, 0.0f, 1.0f) * 255.0f + 0.5f);
@@ -22,65 +20,15 @@ ImU32 ToImGuiColor(const Math::Color& color) {
toChannel(color.a));
}
void DrawAxisLabel(ImDrawList* drawList, const ImVec2& position, const char* label, ImU32 color) {
if (drawList == nullptr || label == nullptr) {
return;
}
const ImVec2 labelSize = ImGui::CalcTextSize(label);
drawList->AddText(
ImVec2(position.x - labelSize.x * 0.5f, position.y - labelSize.y * 0.5f),
color,
label);
}
void DrawSceneAxisWidget(
ImDrawList* drawList,
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax) {
if (drawList == nullptr || !overlay.valid) {
return;
}
const Math::Matrix4x4 view = BuildOverlayViewMatrix(overlay);
const ImVec2 center(viewportMax.x - 52.0f, viewportMin.y + 52.0f);
const float radius = 25.0f;
drawList->AddCircleFilled(center, radius + 12.0f, IM_COL32(17, 19, 22, 178), 24);
drawList->AddCircle(center, radius + 12.0f, IM_COL32(255, 255, 255, 30), 24, 1.0f);
struct AxisLine {
Math::Vector3 axis;
const char* label;
ImU32 color;
};
const AxisLine axes[] = {
{ Math::Vector3::Right(), "x", IM_COL32(239, 83, 80, 255) },
{ Math::Vector3::Up(), "y", IM_COL32(102, 187, 106, 255) },
{ Math::Vector3::Forward(), "z", IM_COL32(66, 165, 245, 255) }
};
for (const AxisLine& axis : axes) {
const Math::Vector3 viewAxis = view.MultiplyVector(axis.axis);
const ImVec2 end(
center.x + viewAxis.x * radius,
center.y - viewAxis.y * radius);
drawList->AddLine(center, end, axis.color, 2.0f);
drawList->AddCircleFilled(end, 6.0f, axis.color, 16);
DrawAxisLabel(drawList, end, axis.label, IM_COL32(245, 245, 245, 255));
}
}
void DrawSceneMoveGizmo(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportMoveGizmoDrawData& moveGizmo) {
if (drawList == nullptr || !moveGizmo.visible) {
return;
}
const ImVec2 pivot(moveGizmo.pivot.x, moveGizmo.pivot.y);
const ImVec2 pivot(viewportMin.x + moveGizmo.pivot.x, viewportMin.y + moveGizmo.pivot.y);
drawList->AddCircleFilled(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(20, 22, 24, 220), 20);
drawList->AddCircle(pivot, moveGizmo.pivotRadius + 1.0f, IM_COL32(255, 255, 255, 48), 20, 1.0f);
@@ -91,8 +39,8 @@ void DrawSceneMoveGizmo(
const ImU32 color = ToImGuiColor(handle.color);
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
const ImVec2 start(handle.start.x, handle.start.y);
const ImVec2 end(handle.end.x, handle.end.y);
const ImVec2 start(viewportMin.x + handle.start.x, viewportMin.y + handle.start.y);
const ImVec2 end(viewportMin.x + handle.end.x, viewportMin.y + handle.end.y);
drawList->AddLine(start, end, color, thickness);
drawList->AddCircleFilled(end, handle.active ? 6.5f : 5.5f, color, 20);
}
@@ -113,10 +61,10 @@ void DrawSceneViewportOverlay(
drawList->PushClipRect(viewportMin, viewportMax, true);
if (overlay.valid) {
DrawSceneAxisWidget(drawList, overlay, viewportMin, viewportMax);
DrawSceneViewportOrientationGizmo(drawList, overlay, viewportMin, viewportMax);
}
if (moveGizmo != nullptr) {
DrawSceneMoveGizmo(drawList, *moveGizmo);
DrawSceneMoveGizmo(drawList, viewportMin, *moveGizmo);
}
drawList->PopClipRect();
}