feat: refine scene viewport gizmos and controls

This commit is contained in:
2026-03-31 21:26:40 +08:00
parent be15bc2fc4
commit 6d3a90ef74
16 changed files with 969 additions and 155 deletions

View File

@@ -4,6 +4,7 @@
#include <imgui.h>
#include <cstdint>
#include <string>
namespace XCEngine {
@@ -59,6 +60,16 @@ struct SceneViewportOverlayData {
float orbitDistance = 6.0f;
};
enum class SceneViewportOrientationAxis : uint8_t {
None = 0,
PositiveX,
NegativeX,
PositiveY,
NegativeY,
PositiveZ,
NegativeZ
};
class IViewportHostService {
public:
virtual ~IViewportHostService() = default;
@@ -70,6 +81,7 @@ public:
IEditorContext& context,
const ImVec2& viewportSize,
const ImVec2& viewportMousePosition) = 0;
virtual void AlignSceneViewToOrientationAxis(SceneViewportOrientationAxis axis) = 0;
virtual SceneViewportOverlayData GetSceneViewOverlayData() const = 0;
virtual void RenderRequestedViewports(
IEditorContext& context,

View File

@@ -36,6 +36,7 @@ public:
m_flySpeed = 5.0f;
m_yawDegrees = -35.0f;
m_pitchDegrees = -20.0f;
m_snapAnimating = false;
UpdatePositionFromFocalPoint();
}
@@ -78,11 +79,53 @@ public:
UpdatePositionFromFocalPoint();
}
void SnapToForward(const Math::Vector3& forward) {
if (forward.SqrMagnitude() <= Math::EPSILON) {
return;
}
const OrientationAngles target = ComputeOrientationAngles(forward);
m_yawDegrees = target.yawDegrees;
m_pitchDegrees = target.pitchDegrees;
m_snapAnimating = false;
UpdatePositionFromFocalPoint();
}
void AnimateToForward(const Math::Vector3& forward) {
if (forward.SqrMagnitude() <= Math::EPSILON) {
return;
}
const OrientationAngles target = ComputeOrientationAngles(forward);
m_snapStartYawDegrees = m_yawDegrees;
m_snapStartPitchDegrees = m_pitchDegrees;
m_snapTargetYawDegrees = target.yawDegrees;
m_snapTargetPitchDegrees = target.pitchDegrees;
m_snapElapsed = 0.0f;
m_snapAnimating = true;
}
void ApplyInput(const SceneViewportCameraInputState& input) {
if (input.viewportHeight <= 0.0f) {
return;
}
const bool hasManualInput =
std::abs(input.lookDeltaX) > Math::EPSILON ||
std::abs(input.lookDeltaY) > Math::EPSILON ||
std::abs(input.orbitDeltaX) > Math::EPSILON ||
std::abs(input.orbitDeltaY) > Math::EPSILON ||
std::abs(input.panDeltaX) > Math::EPSILON ||
std::abs(input.panDeltaY) > Math::EPSILON ||
std::abs(input.zoomDelta) > Math::EPSILON ||
std::abs(input.flySpeedDelta) > Math::EPSILON ||
std::abs(input.moveForward) > Math::EPSILON ||
std::abs(input.moveRight) > Math::EPSILON ||
std::abs(input.moveUp) > Math::EPSILON;
if (hasManualInput) {
m_snapAnimating = false;
}
if (std::abs(input.lookDeltaX) > Math::EPSILON ||
std::abs(input.lookDeltaY) > Math::EPSILON) {
ApplyRotationDelta(input.lookDeltaX, input.lookDeltaY);
@@ -108,7 +151,7 @@ public:
if (std::abs(input.flySpeedDelta) > Math::EPSILON) {
const float speedFactor = std::pow(1.20f, input.flySpeedDelta);
m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.5f, 500.0f);
m_flySpeed = std::clamp(m_flySpeed * speedFactor, 0.1f, 500.0f);
}
if (input.deltaTime > 0.0f &&
@@ -133,6 +176,18 @@ public:
m_distance = std::clamp(m_distance * zoomFactor, 0.5f, 500.0f);
UpdatePositionFromFocalPoint();
}
if (m_snapAnimating && input.deltaTime > 0.0f) {
m_snapElapsed += input.deltaTime;
const float t = std::clamp(m_snapElapsed / kSnapDurationSeconds, 0.0f, 1.0f);
const float easedT = 1.0f - std::pow(1.0f - t, 3.0f);
m_yawDegrees = LerpAngleDegrees(m_snapStartYawDegrees, m_snapTargetYawDegrees, easedT);
m_pitchDegrees = m_snapStartPitchDegrees + (m_snapTargetPitchDegrees - m_snapStartPitchDegrees) * easedT;
UpdatePositionFromFocalPoint();
if (t >= 1.0f) {
m_snapAnimating = false;
}
}
}
void ApplyTo(Components::TransformComponent& transform) const {
@@ -144,6 +199,46 @@ public:
}
private:
static constexpr float kSnapDurationSeconds = 0.22f;
struct OrientationAngles {
float yawDegrees = 0.0f;
float pitchDegrees = 0.0f;
};
static float NormalizeDegrees(float value) {
while (value > 180.0f) {
value -= 360.0f;
}
while (value < -180.0f) {
value += 360.0f;
}
return value;
}
static float LerpAngleDegrees(float fromDegrees, float toDegrees, float t) {
const float delta = NormalizeDegrees(toDegrees - fromDegrees);
return NormalizeDegrees(fromDegrees + delta * t);
}
static OrientationAngles ComputeOrientationAngles(const Math::Vector3& forward) {
OrientationAngles result = {};
const Math::Vector3 normalizedForward = forward.Normalized();
const float horizontalLengthSq =
normalizedForward.x * normalizedForward.x +
normalizedForward.z * normalizedForward.z;
if (horizontalLengthSq <= Math::EPSILON) {
result.yawDegrees = 0.0f;
} else {
result.yawDegrees = std::atan2(normalizedForward.x, normalizedForward.z) * Math::RAD_TO_DEG;
}
result.pitchDegrees = std::clamp(
std::asin(std::clamp(normalizedForward.y, -1.0f, 1.0f)) * Math::RAD_TO_DEG,
-89.0f,
89.0f);
return result;
}
void ApplyRotationDelta(float deltaX, float deltaY) {
m_yawDegrees += deltaX * 0.30f;
m_pitchDegrees = std::clamp(m_pitchDegrees - deltaY * 0.20f, -89.0f, 89.0f);
@@ -180,6 +275,12 @@ private:
float m_flySpeed = 5.0f;
float m_yawDegrees = -35.0f;
float m_pitchDegrees = -20.0f;
bool m_snapAnimating = false;
float m_snapElapsed = 0.0f;
float m_snapStartYawDegrees = 0.0f;
float m_snapStartPitchDegrees = 0.0f;
float m_snapTargetYawDegrees = 0.0f;
float m_snapTargetPitchDegrees = 0.0f;
};
} // namespace Editor

View File

@@ -88,6 +88,8 @@ PSOutput MainPS(VSOutput input) {
const float tanHalfFov = max(gCameraUpAndTanHalfFov.w, 1e-4);
const float aspect = max(gCameraForwardAndAspect.w, 1e-4);
const float transitionBlend = saturate(gGridTransition.x);
const float nearClip = gViewportNearFar.z;
const float sceneFarClip = gViewportNearFar.w;
const float2 ndc = float2(
(input.position.x / viewportSize.x) * 2.0 - 1.0,
@@ -104,19 +106,22 @@ PSOutput MainPS(VSOutput input) {
}
const float t = -cameraPosition.y / rayDirection.y;
if (t <= gViewportNearFar.z || t >= gViewportNearFar.w) {
if (t <= nearClip) {
discard;
}
const float3 worldPosition = cameraPosition + rayDirection * t;
const float4 clipPosition = mul(gViewProjectionMatrix, float4(worldPosition, 1.0));
if (clipPosition.w <= 1e-6) {
discard;
}
float depth = 0.999999;
if (t < sceneFarClip) {
const float4 clipPosition = mul(gViewProjectionMatrix, float4(worldPosition, 1.0));
if (clipPosition.w <= 1e-6) {
discard;
}
const float depth = clipPosition.z / clipPosition.w;
if (depth <= 0.0 || depth >= 1.0) {
discard;
depth = clipPosition.z / clipPosition.w;
if (depth <= 0.0 || depth >= 1.0) {
discard;
}
}
const float radialFade =

View File

@@ -107,6 +107,70 @@ inline bool ProjectSceneViewportAxisDirection(
return true;
}
inline bool ProjectSceneViewportAxisDirectionAtPoint(
const SceneViewportOverlayData& overlay,
float viewportWidth,
float viewportHeight,
const Math::Vector3& worldPoint,
const Math::Vector3& worldAxis,
Math::Vector2& outScreenDirection,
float sampleDistance = 1.0f) {
const Math::Vector3 axis = worldAxis.Normalized();
if (!overlay.valid ||
viewportWidth <= 1.0f ||
viewportHeight <= 1.0f ||
axis.SqrMagnitude() <= Math::EPSILON ||
sampleDistance <= Math::EPSILON) {
return false;
}
const Math::Matrix4x4 viewProjection =
BuildSceneViewportViewProjectionMatrix(overlay, viewportWidth, viewportHeight);
const Math::Vector4 startClip = viewProjection * Math::Vector4(worldPoint, 1.0f);
const Math::Vector4 endClip = viewProjection * Math::Vector4(worldPoint + axis * sampleDistance, 1.0f);
if (startClip.w <= Math::EPSILON || endClip.w <= Math::EPSILON) {
return ProjectSceneViewportAxisDirection(overlay, axis, outScreenDirection);
}
const Math::Vector3 startNdc = startClip.ToVector3() / startClip.w;
const Math::Vector3 endNdc = endClip.ToVector3() / endClip.w;
const Math::Vector2 startScreen(
(startNdc.x * 0.5f + 0.5f) * viewportWidth,
(1.0f - (startNdc.y * 0.5f + 0.5f)) * viewportHeight);
const Math::Vector2 endScreen(
(endNdc.x * 0.5f + 0.5f) * viewportWidth,
(1.0f - (endNdc.y * 0.5f + 0.5f)) * viewportHeight);
const Math::Vector2 screenDirection = endScreen - startScreen;
if (screenDirection.SqrMagnitude() <= Math::EPSILON) {
return ProjectSceneViewportAxisDirection(overlay, axis, outScreenDirection);
}
outScreenDirection = screenDirection.Normalized();
return true;
}
inline bool ProjectSceneViewportWorldPointClamped(
const SceneViewportOverlayData& overlay,
float viewportWidth,
float viewportHeight,
const Math::Vector3& worldPoint,
float edgePadding,
SceneViewportProjectedPoint& outProjectedPoint) {
outProjectedPoint = ProjectSceneViewportWorldPoint(overlay, viewportWidth, viewportHeight, worldPoint);
if (!overlay.valid || viewportWidth <= 1.0f || viewportHeight <= 1.0f || outProjectedPoint.ndcDepth < 0.0f) {
return false;
}
const float minX = edgePadding;
const float minY = edgePadding;
const float maxX = viewportWidth - edgePadding;
const float maxY = viewportHeight - edgePadding;
outProjectedPoint.screenPosition.x = std::clamp(outProjectedPoint.screenPosition.x, minX, maxX);
outProjectedPoint.screenPosition.y = std::clamp(outProjectedPoint.screenPosition.y, minY, maxY);
return true;
}
inline float DistanceToSegmentSquared(
const Math::Vector2& point,
const Math::Vector2& segmentStart,

View File

@@ -14,7 +14,7 @@ namespace Editor {
namespace {
constexpr float kMoveGizmoHandleLengthPixels = 72.0f;
constexpr float kMoveGizmoHandleLengthPixels = 88.0f;
constexpr float kMoveGizmoHoverThresholdPixels = 10.0f;
Math::Vector3 GetAxisVector(SceneViewportGizmoAxis axis) {
@@ -45,10 +45,120 @@ Math::Color GetAxisBaseColor(SceneViewportGizmoAxis axis) {
}
}
Math::Color WithAlpha(const Math::Color& color, float alpha) {
return Math::Color(color.r, color.g, color.b, alpha);
}
SceneViewportGizmoPlane GetPlaneForIndex(size_t index) {
switch (index) {
case 0:
return SceneViewportGizmoPlane::XY;
case 1:
return SceneViewportGizmoPlane::XZ;
case 2:
return SceneViewportGizmoPlane::YZ;
default:
return SceneViewportGizmoPlane::None;
}
}
void GetPlaneAxes(
SceneViewportGizmoPlane plane,
Math::Vector3& outAxisA,
Math::Vector3& outAxisB) {
switch (plane) {
case SceneViewportGizmoPlane::XY:
outAxisA = Math::Vector3::Right();
outAxisB = Math::Vector3::Up();
return;
case SceneViewportGizmoPlane::XZ:
outAxisA = Math::Vector3::Right();
outAxisB = Math::Vector3::Forward();
return;
case SceneViewportGizmoPlane::YZ:
outAxisA = Math::Vector3::Up();
outAxisB = Math::Vector3::Forward();
return;
case SceneViewportGizmoPlane::None:
default:
outAxisA = Math::Vector3::Zero();
outAxisB = Math::Vector3::Zero();
return;
}
}
Math::Vector3 GetPlaneNormal(SceneViewportGizmoPlane plane) {
switch (plane) {
case SceneViewportGizmoPlane::XY:
return Math::Vector3::Forward();
case SceneViewportGizmoPlane::XZ:
return Math::Vector3::Up();
case SceneViewportGizmoPlane::YZ:
return Math::Vector3::Right();
case SceneViewportGizmoPlane::None:
default:
return Math::Vector3::Zero();
}
}
Math::Color GetPlaneBaseColor(SceneViewportGizmoPlane plane) {
switch (plane) {
case SceneViewportGizmoPlane::XY:
return GetAxisBaseColor(SceneViewportGizmoAxis::Z);
case SceneViewportGizmoPlane::XZ:
return GetAxisBaseColor(SceneViewportGizmoAxis::Y);
case SceneViewportGizmoPlane::YZ:
return GetAxisBaseColor(SceneViewportGizmoAxis::X);
case SceneViewportGizmoPlane::None:
default:
return Math::Color::White();
}
}
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
}
float Cross2D(const Math::Vector2& a, const Math::Vector2& b) {
return a.x * b.y - a.y * b.x;
}
float PolygonAreaTwice(const std::array<Math::Vector2, 4>& corners) {
float areaTwice = 0.0f;
for (size_t index = 0; index < corners.size(); ++index) {
const Math::Vector2& current = corners[index];
const Math::Vector2& next = corners[(index + 1) % corners.size()];
areaTwice += current.x * next.y - next.x * current.y;
}
return areaTwice;
}
bool PointInTriangle(
const Math::Vector2& point,
const Math::Vector2& a,
const Math::Vector2& b,
const Math::Vector2& c) {
const float ab = Cross2D(b - a, point - a);
const float bc = Cross2D(c - b, point - b);
const float ca = Cross2D(a - c, point - c);
const bool hasNegative = ab < 0.0f || bc < 0.0f || ca < 0.0f;
const bool hasPositive = ab > 0.0f || bc > 0.0f || ca > 0.0f;
return !(hasNegative && hasPositive);
}
bool PointInQuad(const Math::Vector2& point, const std::array<Math::Vector2, 4>& corners) {
return PointInTriangle(point, corners[0], corners[1], corners[2]) ||
PointInTriangle(point, corners[0], corners[2], corners[3]);
}
Math::Vector2 QuadCenter(const std::array<Math::Vector2, 4>& corners) {
Math::Vector2 center = Math::Vector2::Zero();
for (const Math::Vector2& corner : corners) {
center += corner;
}
return center / 4.0f;
}
bool IsMouseInsideViewport(const SceneViewportMoveGizmoContext& context) {
return context.mousePosition.x >= 0.0f &&
context.mousePosition.y >= 0.0f &&
@@ -141,20 +251,28 @@ Math::Vector3 GetGizmoWorldOrigin(const Components::GameObject& gameObject) {
void SceneViewportMoveGizmo::Update(const SceneViewportMoveGizmoContext& context) {
BuildDrawData(context);
if (m_activeAxis == SceneViewportGizmoAxis::None && IsMouseInsideViewport(context)) {
if (m_dragMode == DragMode::None && IsMouseInsideViewport(context)) {
m_hoveredAxis = HitTestAxis(context.mousePosition);
} else if (m_activeAxis == SceneViewportGizmoAxis::None) {
m_hoveredPlane = m_hoveredAxis == SceneViewportGizmoAxis::None
? HitTestPlane(context.mousePosition)
: SceneViewportGizmoPlane::None;
} else if (m_dragMode == DragMode::None) {
m_hoveredAxis = SceneViewportGizmoAxis::None;
} else {
m_hoveredPlane = SceneViewportGizmoPlane::None;
} else if (m_dragMode == DragMode::Axis) {
m_hoveredAxis = m_activeAxis;
m_hoveredPlane = SceneViewportGizmoPlane::None;
} else {
m_hoveredAxis = SceneViewportGizmoAxis::None;
m_hoveredPlane = m_activePlane;
}
RefreshHandleState();
}
bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& context, IUndoManager& undoManager) {
if (m_activeAxis != SceneViewportGizmoAxis::None ||
m_hoveredAxis == SceneViewportGizmoAxis::None ||
if (m_dragMode != DragMode::None ||
(m_hoveredAxis == SceneViewportGizmoAxis::None && m_hoveredPlane == SceneViewportGizmoPlane::None) ||
context.selectedObject == nullptr ||
!m_drawData.visible ||
undoManager.HasPendingInteractiveChange()) {
@@ -170,14 +288,23 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
return false;
}
const Math::Vector3 worldAxis = GetAxisVector(m_hoveredAxis);
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
return false;
}
const Math::Vector3 objectWorldPosition = context.selectedObject->GetTransform()->GetPosition();
const Math::Vector3 pivotWorldPosition = GetGizmoWorldOrigin(*context.selectedObject);
Math::Vector3 dragPlaneNormal = Math::Vector3::Zero();
Math::Vector3 worldAxis = Math::Vector3::Zero();
if (m_hoveredAxis != SceneViewportGizmoAxis::None) {
worldAxis = GetAxisVector(m_hoveredAxis);
if (!BuildSceneViewportAxisDragPlaneNormal(context.overlay, worldAxis, dragPlaneNormal)) {
return false;
}
} else {
dragPlaneNormal = GetPlaneNormal(m_hoveredPlane);
if (dragPlaneNormal.SqrMagnitude() <= Math::EPSILON) {
return false;
}
}
const Math::Plane dragPlane = BuildSceneViewportPlaneFromPointNormal(pivotWorldPosition, dragPlaneNormal);
float hitDistance = 0.0f;
if (!worldRay.Intersects(dragPlane, hitDistance)) {
@@ -190,19 +317,23 @@ bool SceneViewportMoveGizmo::TryBeginDrag(const SceneViewportMoveGizmoContext& c
}
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
m_dragMode = m_hoveredAxis != SceneViewportGizmoAxis::None ? DragMode::Axis : DragMode::Plane;
m_activeAxis = m_hoveredAxis;
m_activePlane = m_hoveredPlane;
m_activeEntityId = context.selectedObject->GetID();
m_activeAxisDirection = worldAxis;
m_activePlaneNormal = dragPlaneNormal;
m_dragPlane = dragPlane;
m_dragStartObjectWorldPosition = objectWorldPosition;
m_dragStartPivotWorldPosition = pivotWorldPosition;
m_dragStartHitWorldPosition = hitPoint;
m_dragStartAxisScalar = Math::Vector3::Dot(hitPoint - pivotWorldPosition, worldAxis);
RefreshHandleState();
return true;
}
void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& context) {
if (m_activeAxis == SceneViewportGizmoAxis::None ||
if (m_dragMode == DragMode::None ||
context.selectedObject == nullptr ||
context.selectedObject->GetID() != m_activeEntityId) {
return;
@@ -223,14 +354,25 @@ void SceneViewportMoveGizmo::UpdateDrag(const SceneViewportMoveGizmoContext& con
}
const Math::Vector3 hitPoint = worldRay.GetPoint(hitDistance);
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
context.selectedObject->GetTransform()->SetPosition(
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
if (m_dragMode == DragMode::Axis) {
const float currentAxisScalar = Math::Vector3::Dot(hitPoint - m_dragStartPivotWorldPosition, m_activeAxisDirection);
const float deltaScalar = currentAxisScalar - m_dragStartAxisScalar;
context.selectedObject->GetTransform()->SetPosition(
m_dragStartObjectWorldPosition + m_activeAxisDirection * deltaScalar);
return;
}
if (m_dragMode == DragMode::Plane) {
const Math::Vector3 planeDelta = Math::Vector3::ProjectOnPlane(
hitPoint - m_dragStartHitWorldPosition,
m_activePlaneNormal);
context.selectedObject->GetTransform()->SetPosition(
m_dragStartObjectWorldPosition + planeDelta);
}
}
void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
if (m_activeAxis == SceneViewportGizmoAxis::None) {
if (m_dragMode == DragMode::None) {
return;
}
@@ -238,11 +380,15 @@ void SceneViewportMoveGizmo::EndDrag(IUndoManager& undoManager) {
undoManager.FinalizeInteractiveChange();
}
m_dragMode = DragMode::None;
m_activeAxis = SceneViewportGizmoAxis::None;
m_activePlane = SceneViewportGizmoPlane::None;
m_activeEntityId = 0;
m_activeAxisDirection = Math::Vector3::Zero();
m_activePlaneNormal = Math::Vector3::Zero();
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragStartHitWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f;
RefreshHandleState();
}
@@ -252,22 +398,28 @@ void SceneViewportMoveGizmo::CancelDrag(IUndoManager* undoManager) {
undoManager->CancelInteractiveChange();
}
m_dragMode = DragMode::None;
m_activeAxis = SceneViewportGizmoAxis::None;
m_activePlane = SceneViewportGizmoPlane::None;
m_activeEntityId = 0;
m_activeAxisDirection = Math::Vector3::Zero();
m_activePlaneNormal = Math::Vector3::Zero();
m_dragStartObjectWorldPosition = Math::Vector3::Zero();
m_dragStartPivotWorldPosition = Math::Vector3::Zero();
m_dragStartHitWorldPosition = Math::Vector3::Zero();
m_dragStartAxisScalar = 0.0f;
m_hoveredAxis = SceneViewportGizmoAxis::None;
m_hoveredPlane = SceneViewportGizmoPlane::None;
RefreshHandleState();
}
bool SceneViewportMoveGizmo::IsHoveringHandle() const {
return m_hoveredAxis != SceneViewportGizmoAxis::None;
return m_hoveredAxis != SceneViewportGizmoAxis::None ||
m_hoveredPlane != SceneViewportGizmoPlane::None;
}
bool SceneViewportMoveGizmo::IsActive() const {
return m_activeAxis != SceneViewportGizmoAxis::None;
return m_dragMode != DragMode::None;
}
uint64_t SceneViewportMoveGizmo::GetActiveEntityId() const {
@@ -312,6 +464,8 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
}
const float axisLengthWorld = worldUnitsPerPixel * kMoveGizmoHandleLengthPixels;
const float planeInsetWorld = axisLengthWorld * 0.02f;
const float planeExtentWorld = axisLengthWorld * 0.36f;
const SceneViewportGizmoAxis axes[] = {
SceneViewportGizmoAxis::X,
@@ -342,6 +496,49 @@ void SceneViewportMoveGizmo::BuildDrawData(const SceneViewportMoveGizmoContext&
handle.visible = true;
handle.color = GetAxisBaseColor(handle.axis);
}
for (size_t index = 0; index < m_drawData.planes.size(); ++index) {
SceneViewportMoveGizmoPlaneDrawData& plane = m_drawData.planes[index];
plane.plane = GetPlaneForIndex(index);
Math::Vector3 axisA = Math::Vector3::Zero();
Math::Vector3 axisB = Math::Vector3::Zero();
GetPlaneAxes(plane.plane, axisA, axisB);
if (axisA.SqrMagnitude() <= Math::EPSILON || axisB.SqrMagnitude() <= Math::EPSILON) {
continue;
}
const Math::Vector3 worldCorners[] = {
gizmoWorldOrigin + axisA * planeInsetWorld + axisB * planeInsetWorld,
gizmoWorldOrigin + axisA * planeExtentWorld + axisB * planeInsetWorld,
gizmoWorldOrigin + axisA * planeExtentWorld + axisB * planeExtentWorld,
gizmoWorldOrigin + axisA * planeInsetWorld + axisB * planeExtentWorld
};
bool valid = true;
for (size_t cornerIndex = 0; cornerIndex < plane.corners.size(); ++cornerIndex) {
const SceneViewportProjectedPoint projectedCorner = ProjectSceneViewportWorldPoint(
context.overlay,
context.viewportSize.x,
context.viewportSize.y,
worldCorners[cornerIndex]);
if (projectedCorner.ndcDepth < 0.0f || projectedCorner.ndcDepth > 1.0f) {
valid = false;
break;
}
plane.corners[cornerIndex] = projectedCorner.screenPosition;
}
if (!valid || std::abs(PolygonAreaTwice(plane.corners)) <= 4.0f) {
continue;
}
plane.visible = true;
const Math::Color baseColor = GetPlaneBaseColor(plane.plane);
plane.fillColor = WithAlpha(baseColor, 0.16f);
plane.outlineColor = WithAlpha(baseColor, 0.88f);
}
}
void SceneViewportMoveGizmo::RefreshHandleState() {
@@ -356,6 +553,20 @@ void SceneViewportMoveGizmo::RefreshHandleState() {
? Math::Color::Yellow()
: GetAxisBaseColor(handle.axis);
}
for (SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) {
if (!plane.visible) {
continue;
}
plane.hovered = plane.plane == m_hoveredPlane;
plane.active = plane.plane == m_activePlane;
const Math::Color baseColor = plane.hovered || plane.active
? Math::Color::Yellow()
: GetPlaneBaseColor(plane.plane);
plane.fillColor = WithAlpha(baseColor, plane.active ? 0.34f : (plane.hovered ? 0.26f : 0.16f));
plane.outlineColor = WithAlpha(baseColor, plane.active ? 1.0f : (plane.hovered ? 0.95f : 0.82f));
}
}
SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2& mousePosition) const {
@@ -384,5 +595,29 @@ SceneViewportGizmoAxis SceneViewportMoveGizmo::HitTestAxis(const Math::Vector2&
return bestAxis;
}
SceneViewportGizmoPlane SceneViewportMoveGizmo::HitTestPlane(const Math::Vector2& mousePosition) const {
if (!m_drawData.visible) {
return SceneViewportGizmoPlane::None;
}
SceneViewportGizmoPlane bestPlane = SceneViewportGizmoPlane::None;
float bestDistanceSq = Math::FLOAT_MAX;
for (const SceneViewportMoveGizmoPlaneDrawData& plane : m_drawData.planes) {
if (!plane.visible || !PointInQuad(mousePosition, plane.corners)) {
continue;
}
const float distanceSq = (QuadCenter(plane.corners) - mousePosition).SqrMagnitude();
if (distanceSq >= bestDistanceSq) {
continue;
}
bestDistanceSq = distanceSq;
bestPlane = plane.plane;
}
return bestPlane;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -26,6 +26,13 @@ enum class SceneViewportGizmoAxis : uint8_t {
Z
};
enum class SceneViewportGizmoPlane : uint8_t {
None = 0,
XY,
XZ,
YZ
};
struct SceneViewportMoveGizmoHandleDrawData {
SceneViewportGizmoAxis axis = SceneViewportGizmoAxis::None;
Math::Vector2 start = Math::Vector2::Zero();
@@ -36,11 +43,22 @@ struct SceneViewportMoveGizmoHandleDrawData {
bool active = false;
};
struct SceneViewportMoveGizmoPlaneDrawData {
SceneViewportGizmoPlane plane = SceneViewportGizmoPlane::None;
std::array<Math::Vector2, 4> corners = {};
Math::Color fillColor = Math::Color::White();
Math::Color outlineColor = Math::Color::White();
bool visible = false;
bool hovered = false;
bool active = false;
};
struct SceneViewportMoveGizmoDrawData {
bool visible = false;
Math::Vector2 pivot = Math::Vector2::Zero();
float pivotRadius = 5.0f;
std::array<SceneViewportMoveGizmoHandleDrawData, 3> handles = {};
std::array<SceneViewportMoveGizmoPlaneDrawData, 3> planes = {};
};
struct SceneViewportMoveGizmoContext {
@@ -64,17 +82,30 @@ public:
const SceneViewportMoveGizmoDrawData& GetDrawData() const;
private:
enum class DragMode : uint8_t {
None = 0,
Axis,
Plane
};
void BuildDrawData(const SceneViewportMoveGizmoContext& context);
void RefreshHandleState();
SceneViewportGizmoAxis HitTestAxis(const Math::Vector2& mousePosition) const;
SceneViewportGizmoPlane HitTestPlane(const Math::Vector2& mousePosition) const;
SceneViewportMoveGizmoDrawData m_drawData = {};
SceneViewportGizmoAxis m_hoveredAxis = SceneViewportGizmoAxis::None;
SceneViewportGizmoPlane m_hoveredPlane = SceneViewportGizmoPlane::None;
SceneViewportGizmoAxis m_activeAxis = SceneViewportGizmoAxis::None;
SceneViewportGizmoPlane m_activePlane = SceneViewportGizmoPlane::None;
DragMode m_dragMode = DragMode::None;
uint64_t m_activeEntityId = 0;
Math::Vector3 m_activeAxisDirection = Math::Vector3::Zero();
Math::Vector3 m_activePlaneNormal = Math::Vector3::Zero();
Math::Plane m_dragPlane = {};
Math::Vector3 m_dragStartWorldPosition = Math::Vector3::Zero();
Math::Vector3 m_dragStartObjectWorldPosition = Math::Vector3::Zero();
Math::Vector3 m_dragStartPivotWorldPosition = Math::Vector3::Zero();
Math::Vector3 m_dragStartHitWorldPosition = Math::Vector3::Zero();
float m_dragStartAxisScalar = 0.0f;
};

View File

@@ -130,6 +130,7 @@ ProjectedPoint ProjectPoint(const ImVec2& center, const Math::Vector3& point) {
}
struct AxisHandleVisual {
SceneViewportOrientationAxis axis = SceneViewportOrientationAxis::None;
Math::Vector3 cameraDirection = Math::Vector3::Zero();
Math::Color baseColor = Math::Color::White();
const char* label = nullptr;
@@ -137,6 +138,17 @@ struct AxisHandleVisual {
float sortDepth = 0.0f;
};
struct AxisHandleProjection {
bool valid = false;
ProjectedPoint tip = {};
ProjectedPoint capCenter = {};
ImVec2 axisDirection = ImVec2(0.0f, 0.0f);
ImVec2 leftPoint = ImVec2(0.0f, 0.0f);
ImVec2 rightPoint = ImVec2(0.0f, 0.0f);
std::array<ImVec2, kConeCapSegments> capPoints = {};
float capMinorSpan = 0.0f;
};
struct CubeFaceVisual {
std::array<ImVec2, 4> points = {};
Math::Vector3 cameraNormal = Math::Vector3::Zero();
@@ -157,6 +169,178 @@ float ComputePolygonSignedArea(const ImVec2* points, int pointCount) {
return area * 0.5f;
}
float ComputeSegmentDistanceSquared(const ImVec2& point, const ImVec2& a, const ImVec2& b) {
const float dx = b.x - a.x;
const float dy = b.y - a.y;
const float lengthSq = dx * dx + dy * dy;
if (lengthSq <= 1e-6f) {
const float px = point.x - a.x;
const float py = point.y - a.y;
return px * px + py * py;
}
const float t = std::clamp(
((point.x - a.x) * dx + (point.y - a.y) * dy) / lengthSq,
0.0f,
1.0f);
const float closestX = a.x + dx * t;
const float closestY = a.y + dy * t;
const float px = point.x - closestX;
const float py = point.y - closestY;
return px * px + py * py;
}
bool PointInTriangle(const ImVec2& point, const ImVec2& a, const ImVec2& b, const ImVec2& c) {
const float ab = (point.x - b.x) * (a.y - b.y) - (a.x - b.x) * (point.y - b.y);
const float bc = (point.x - c.x) * (b.y - c.y) - (b.x - c.x) * (point.y - c.y);
const float ca = (point.x - a.x) * (c.y - a.y) - (c.x - a.x) * (point.y - a.y);
const bool hasNegative = ab < 0.0f || bc < 0.0f || ca < 0.0f;
const bool hasPositive = ab > 0.0f || bc > 0.0f || ca > 0.0f;
return !(hasNegative && hasPositive);
}
bool PointInConvexPolygon(const ImVec2& point, const ImVec2* polygon, int pointCount) {
if (polygon == nullptr || pointCount < 3) {
return false;
}
float sign = 0.0f;
for (int i = 0; i < pointCount; ++i) {
const ImVec2& a = polygon[i];
const ImVec2& b = polygon[(i + 1) % pointCount];
const float cross = (point.x - a.x) * (b.y - a.y) - (point.y - a.y) * (b.x - a.x);
if (std::abs(cross) <= 1e-4f) {
continue;
}
if (sign == 0.0f) {
sign = cross;
continue;
}
if ((sign > 0.0f) != (cross > 0.0f)) {
return false;
}
}
return true;
}
ImVec2 BuildWidgetCenter(const ImVec2& viewportMin, const ImVec2& viewportMax) {
return ImVec2(viewportMax.x - kWidgetInset, viewportMin.y + kWidgetInset);
}
std::array<AxisHandleVisual, 6> BuildAxisHandles(const SceneViewportOverlayData& overlay) {
return {{
{ SceneViewportOrientationAxis::PositiveX, TransformToCameraSpace(overlay, Math::Vector3::Right()), Math::Color(0.91f, 0.09f, 0.05f, 1.0f), "x", true, 0.0f },
{ SceneViewportOrientationAxis::NegativeX, TransformToCameraSpace(overlay, Math::Vector3::Left()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
{ SceneViewportOrientationAxis::PositiveY, TransformToCameraSpace(overlay, Math::Vector3::Up()), Math::Color(0.45f, 1.0f, 0.12f, 1.0f), "y", true, 0.0f },
{ SceneViewportOrientationAxis::NegativeY, TransformToCameraSpace(overlay, Math::Vector3::Down()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f },
{ SceneViewportOrientationAxis::PositiveZ, TransformToCameraSpace(overlay, Math::Vector3::Forward()), Math::Color(0.11f, 0.29f, 1.0f, 1.0f), "z", true, 0.0f },
{ SceneViewportOrientationAxis::NegativeZ, TransformToCameraSpace(overlay, Math::Vector3::Back()), Math::Color(1.0f, 1.0f, 1.0f, 1.0f), nullptr, false, 0.0f }
}};
}
std::vector<AxisHandleVisual> BuildSortedAxisHandles(const SceneViewportOverlayData& overlay) {
const std::array<AxisHandleVisual, 6> handles = BuildAxisHandles(overlay);
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;
});
return sortedHandles;
}
AxisHandleProjection BuildAxisHandleProjection(const ImVec2& center, const AxisHandleVisual& handle) {
AxisHandleProjection projection = {};
if (handle.cameraDirection.SqrMagnitude() <= Math::EPSILON) {
return projection;
}
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;
projection.tip = ProjectPoint(center, tipPoint3);
projection.capCenter = ProjectPoint(center, capCenter3);
const ImVec2 axisVector(
projection.capCenter.position.x - projection.tip.position.x,
projection.capCenter.position.y - projection.tip.position.y);
const float axisLengthSq = axisVector.x * axisVector.x + axisVector.y * axisVector.y;
const float axisLength = std::sqrt(axisLengthSq);
projection.axisDirection = axisLength > 1e-4f
? ImVec2(axisVector.x / axisLength, axisVector.y / axisLength)
: ImVec2(0.0f, 0.0f);
const ImVec2 sideDirection(-projection.axisDirection.y, projection.axisDirection.x);
Math::Vector3 capTangent = Math::Vector3::Right();
Math::Vector3 capBitangent = Math::Vector3::Up();
BuildPerpendicularBasis(handle.cameraDirection, capTangent, capBitangent);
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);
projection.capPoints[i] = ProjectPoint(center, ringPoint).position;
const ImVec2 offset(
projection.capPoints[i].x - projection.capCenter.position.x,
projection.capPoints[i].y - projection.capCenter.position.y);
const float sideValue = offset.x * sideDirection.x + offset.y * sideDirection.y;
const float axisValue = offset.x * projection.axisDirection.x + offset.y * projection.axisDirection.y;
if (sideValue > maxSide) {
maxSide = sideValue;
projection.leftPoint = projection.capPoints[i];
}
if (sideValue < minSide) {
minSide = sideValue;
projection.rightPoint = projection.capPoints[i];
}
maxAxis = std::max(maxAxis, axisValue);
minAxis = std::min(minAxis, axisValue);
}
projection.capMinorSpan = maxAxis - minAxis;
projection.valid = true;
return projection;
}
bool HitTestAxisHandle(const ImVec2& mousePosition, const ImVec2& center, const AxisHandleVisual& handle) {
const AxisHandleProjection projection = BuildAxisHandleProjection(center, handle);
if (!projection.valid) {
return false;
}
if (projection.capMinorSpan <= 1.35f) {
if (ComputeSegmentDistanceSquared(mousePosition, projection.rightPoint, projection.leftPoint) <= 16.0f) {
return true;
}
} else if (PointInConvexPolygon(mousePosition, projection.capPoints.data(), static_cast<int>(projection.capPoints.size()))) {
return true;
}
return PointInTriangle(
mousePosition,
projection.tip.position,
projection.leftPoint,
projection.rightPoint);
}
void DrawAxisLabel(ImDrawList* drawList, const ImVec2& position, const char* label) {
if (drawList == nullptr || label == nullptr) {
return;
@@ -283,59 +467,9 @@ void DrawAxisHandle(
}
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 AxisHandleProjection projection = BuildAxisHandleProjection(center, handle);
if (!projection.valid) {
return;
}
const Math::Color bodyColor = handle.positive
@@ -348,58 +482,60 @@ void DrawAxisHandle(
: 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)
ImVec2(projection.tip.position.x + 1.2f, projection.tip.position.y + 1.4f),
ImVec2(projection.leftPoint.x + 1.2f, projection.leftPoint.y + 1.4f),
ImVec2(projection.rightPoint.x + 1.2f, projection.rightPoint.y + 1.4f)
};
const ImVec2 leftFacet[] = {
tip.position,
leftPoint,
capCenter.position
projection.tip.position,
projection.leftPoint,
projection.capCenter.position
};
const ImVec2 rightFacet[] = {
tip.position,
capCenter.position,
rightPoint
projection.tip.position,
projection.capCenter.position,
projection.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(projection.tip.position, projection.leftPoint, IM_COL32(255, 255, 255, handle.positive ? 34 : 44), 1.0f);
drawList->AddLine(projection.tip.position, projection.rightPoint, IM_COL32(0, 0, 0, 38), 1.0f);
if (projection.capMinorSpan <= 1.35f) {
drawList->AddLine(
ImVec2(rightPoint.x + 1.0f, rightPoint.y + 1.2f),
ImVec2(leftPoint.x + 1.0f, leftPoint.y + 1.2f),
ImVec2(projection.rightPoint.x + 1.0f, projection.rightPoint.y + 1.2f),
ImVec2(projection.leftPoint.x + 1.0f, projection.leftPoint.y + 1.2f),
IM_COL32(0, 0, 0, 52),
2.4f);
drawList->AddLine(
rightPoint,
leftPoint,
projection.rightPoint,
projection.leftPoint,
ToImGuiColor(capColor),
2.0f);
drawList->AddLine(
rightPoint,
leftPoint,
projection.rightPoint,
projection.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->AddConvexPolyFilled(projection.capPoints.data(), static_cast<int>(projection.capPoints.size()), ToImGuiColor(capColor));
drawList->AddPolyline(
capPoints.data(),
static_cast<int>(capPoints.size()),
projection.capPoints.data(),
static_cast<int>(projection.capPoints.size()),
IM_COL32(255, 255, 255, handle.positive ? 60 : 72),
true,
1.0f);
}
if (handle.positive && handle.label != nullptr) {
const float axisLength = std::sqrt(
(projection.capCenter.position.x - projection.tip.position.x) * (projection.capCenter.position.x - projection.tip.position.x) +
(projection.capCenter.position.y - projection.tip.position.y) * (projection.capCenter.position.y - projection.tip.position.y));
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);
projection.capCenter.position.x + projection.axisDirection.x * labelOffset,
projection.capCenter.position.y + projection.axisDirection.y * labelOffset);
DrawAxisLabel(drawList, labelPosition, handle.label);
}
}
@@ -415,27 +551,8 @@ void DrawSceneViewportOrientationGizmo(
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;
});
const ImVec2 center = BuildWidgetCenter(viewportMin, viewportMax);
const std::vector<AxisHandleVisual> sortedHandles = BuildSortedAxisHandles(overlay);
for (const AxisHandleVisual& handle : sortedHandles) {
if (handle.sortDepth > 0.0f) {
@@ -452,5 +569,32 @@ void DrawSceneViewportOrientationGizmo(
}
}
SceneViewportOrientationAxis HitTestSceneViewportOrientationGizmo(
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax,
const ImVec2& mousePosition) {
if (!overlay.valid) {
return SceneViewportOrientationAxis::None;
}
const ImVec2 center = BuildWidgetCenter(viewportMin, viewportMax);
std::vector<AxisHandleVisual> handles = BuildSortedAxisHandles(overlay);
std::sort(
handles.begin(),
handles.end(),
[](const AxisHandleVisual& lhs, const AxisHandleVisual& rhs) {
return lhs.sortDepth < rhs.sortDepth;
});
for (const AxisHandleVisual& handle : handles) {
if (HitTestAxisHandle(mousePosition, center, handle)) {
return handle.axis;
}
}
return SceneViewportOrientationAxis::None;
}
} // namespace Editor
} // namespace XCEngine

View File

@@ -13,5 +13,11 @@ void DrawSceneViewportOrientationGizmo(
const ImVec2& viewportMin,
const ImVec2& viewportMax);
SceneViewportOrientationAxis HitTestSceneViewportOrientationGizmo(
const SceneViewportOverlayData& overlay,
const ImVec2& viewportMin,
const ImVec2& viewportMax,
const ImVec2& mousePosition);
} // namespace Editor
} // namespace XCEngine

View File

@@ -2,12 +2,16 @@
#include "SceneViewportOrientationGizmo.h"
#include <algorithm>
#include <cmath>
namespace XCEngine {
namespace Editor {
namespace {
constexpr float kMoveGizmoArrowLength = 14.0f;
constexpr float kMoveGizmoArrowHalfWidth = 7.0f;
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);
@@ -20,6 +24,69 @@ ImU32 ToImGuiColor(const Math::Color& color) {
toChannel(color.a));
}
ImVec2 NormalizeImVec2(const ImVec2& value, const ImVec2& fallback = ImVec2(1.0f, 0.0f)) {
const float lengthSq = value.x * value.x + value.y * value.y;
if (lengthSq <= 1e-6f) {
return fallback;
}
const float inverseLength = 1.0f / std::sqrt(lengthSq);
return ImVec2(value.x * inverseLength, value.y * inverseLength);
}
void DrawSceneMoveGizmoPlane(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportMoveGizmoPlaneDrawData& plane) {
if (drawList == nullptr || !plane.visible) {
return;
}
ImVec2 points[4] = {};
for (size_t index = 0; index < plane.corners.size(); ++index) {
points[index] = ImVec2(
viewportMin.x + plane.corners[index].x,
viewportMin.y + plane.corners[index].y);
}
drawList->AddConvexPolyFilled(points, 4, ToImGuiColor(plane.fillColor));
drawList->AddPolyline(
points,
4,
ToImGuiColor(plane.outlineColor),
true,
plane.active ? 2.6f : (plane.hovered ? 2.0f : 1.4f));
}
void DrawSceneMoveGizmoAxis(
ImDrawList* drawList,
const ImVec2& viewportMin,
const SceneViewportMoveGizmoHandleDrawData& handle) {
if (drawList == nullptr || !handle.visible) {
return;
}
const ImU32 color = ToImGuiColor(handle.color);
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
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);
const ImVec2 direction = NormalizeImVec2(ImVec2(end.x - start.x, end.y - start.y));
const ImVec2 normal(-direction.y, direction.x);
const ImVec2 arrowBase(
end.x - direction.x * kMoveGizmoArrowLength,
end.y - direction.y * kMoveGizmoArrowLength);
const ImVec2 arrowLeft(
arrowBase.x + normal.x * kMoveGizmoArrowHalfWidth,
arrowBase.y + normal.y * kMoveGizmoArrowHalfWidth);
const ImVec2 arrowRight(
arrowBase.x - normal.x * kMoveGizmoArrowHalfWidth,
arrowBase.y - normal.y * kMoveGizmoArrowHalfWidth);
drawList->AddLine(start, arrowBase, color, thickness);
const ImVec2 triangle[3] = { end, arrowLeft, arrowRight };
drawList->AddConvexPolyFilled(triangle, 3, color);
}
void DrawSceneMoveGizmo(
ImDrawList* drawList,
const ImVec2& viewportMin,
@@ -29,21 +96,16 @@ void DrawSceneMoveGizmo(
}
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);
for (const SceneViewportMoveGizmoPlaneDrawData& plane : moveGizmo.planes) {
DrawSceneMoveGizmoPlane(drawList, viewportMin, plane);
}
for (const SceneViewportMoveGizmoHandleDrawData& handle : moveGizmo.handles) {
if (!handle.visible) {
continue;
}
const ImU32 color = ToImGuiColor(handle.color);
const float thickness = handle.active ? 4.0f : (handle.hovered ? 3.0f : 2.0f);
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);
DrawSceneMoveGizmoAxis(drawList, viewportMin, handle);
}
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);
}
} // namespace

View File

@@ -102,7 +102,9 @@ bool SceneViewportSelectionMaskPass::Render(
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface,
const Rendering::RenderCameraData& cameraData,
const std::vector<Rendering::VisibleRenderItem>& renderables) {
const std::vector<Rendering::VisibleRenderItem>& renderables,
bool debugColor) {
(void)debugColor;
if (!renderContext.IsValid() ||
renderContext.backendType != RHI::RHIType::D3D12 ||
renderables.empty()) {

View File

@@ -29,7 +29,8 @@ public:
const Rendering::RenderContext& renderContext,
const Rendering::RenderSurface& surface,
const Rendering::RenderCameraData& cameraData,
const std::vector<Rendering::VisibleRenderItem>& renderables);
const std::vector<Rendering::VisibleRenderItem>& renderables,
bool debugColor = false);
private:
struct OwnedDescriptorSet {

View File

@@ -10,7 +10,9 @@ namespace Editor {
GameViewPanel::GameViewPanel() : Panel("Game") {}
void GameViewPanel::Render() {
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
UI::PanelWindowScope panel(m_name.c_str());
ImGui::PopStyleVar();
if (!panel.IsOpen()) {
return;
}

View File

@@ -36,6 +36,7 @@ XCEngine::Editor::UI::TreeNodeDefinition BuildHierarchyNodeDefinition(
XCEngine::Editor::UI::TreeNodeDefinition nodeDefinition;
nodeDefinition.options.selected = context.GetSelectionManager().IsSelected(gameObject->GetID());
nodeDefinition.options.leaf = gameObject->GetChildCount() == 0;
nodeDefinition.options.openOnDoubleClick = false;
nodeDefinition.style = XCEngine::Editor::UI::HierarchyTreeStyle();
nodeDefinition.prefix.width = XCEngine::Editor::UI::NavigationTreePrefixWidth();
nodeDefinition.prefix.draw = DrawHierarchyTreePrefix;

View File

@@ -3,6 +3,7 @@
#include "Core/ISceneManager.h"
#include "Core/ISelectionManager.h"
#include "SceneViewPanel.h"
#include "Viewport/SceneViewportOrientationGizmo.h"
#include "Viewport/SceneViewportOverlayRenderer.h"
#include "ViewportPanelContent.h"
#include "UI/UI.h"
@@ -75,24 +76,48 @@ void SceneViewPanel::Render() {
!m_lookDragging &&
!m_panDragging &&
m_moveGizmo.IsHoveringHandle();
const SceneViewportOrientationAxis orientationAxisHit =
hasInteractiveViewport &&
content.hovered &&
!m_lookDragging &&
!m_panDragging &&
!m_moveGizmo.IsHoveringHandle() &&
!m_moveGizmo.IsActive()
? HitTestSceneViewportOrientationGizmo(
overlay,
content.itemMin,
content.itemMax,
io.MousePos)
: SceneViewportOrientationAxis::None;
const bool orientationGizmoClick =
hasInteractiveViewport &&
content.hovered &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
orientationAxisHit != SceneViewportOrientationAxis::None;
const bool selectClick =
hasInteractiveViewport &&
content.hovered &&
ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!m_lookDragging &&
!m_panDragging &&
!orientationGizmoClick &&
!m_moveGizmo.IsHoveringHandle() &&
!m_moveGizmo.IsActive();
const bool beginLookDrag =
hasInteractiveViewport &&
content.hovered &&
!m_lookDragging &&
!m_moveGizmo.IsActive() &&
ImGui::IsMouseClicked(ImGuiMouseButton_Right);
const bool beginPanDrag =
hasInteractiveViewport &&
content.hovered &&
!m_panDragging &&
!m_moveGizmo.IsActive() &&
!m_lookDragging &&
ImGui::IsMouseClicked(ImGuiMouseButton_Middle);
if (beginMoveGizmo || selectClick || beginLookDrag || beginPanDrag) {
if (beginMoveGizmo || orientationGizmoClick || selectClick || beginLookDrag || beginPanDrag) {
ImGui::SetWindowFocus();
}
@@ -100,6 +125,13 @@ void SceneViewPanel::Render() {
m_moveGizmo.TryBeginDrag(moveGizmoContext, m_context->GetUndoManager());
}
if (orientationGizmoClick) {
viewportHostService->AlignSceneViewToOrientationAxis(orientationAxisHit);
overlay = viewportHostService->GetSceneViewOverlayData();
moveGizmoContext = BuildMoveGizmoContext(*m_context, overlay, content, io.MousePos);
m_moveGizmo.Update(moveGizmoContext);
}
if (selectClick) {
const ImVec2 localMousePosition(
io.MousePos.x - content.itemMin.x,
@@ -125,11 +157,15 @@ void SceneViewPanel::Render() {
if (beginLookDrag) {
m_lookDragging = true;
m_panDragging = false;
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
}
if (beginPanDrag) {
m_panDragging = true;
m_lookDragging = false;
m_lastPanDragDelta = ImVec2(0.0f, 0.0f);
m_lastLookDragDelta = ImVec2(0.0f, 0.0f);
}
if (m_lookDragging && !ImGui::IsMouseDown(ImGuiMouseButton_Right)) {

View File

@@ -2,6 +2,7 @@
#include "Core/IEditorContext.h"
#include "Viewport/IViewportHostService.h"
#include "UI/UI.h"
#include <imgui.h>
@@ -18,6 +19,9 @@ struct ViewportPanelContentResult {
bool hasViewportArea = false;
bool hovered = false;
bool focused = false;
bool clickedLeft = false;
bool clickedRight = false;
bool clickedMiddle = false;
};
inline void DrawViewportStatusMessage(const std::string& message) {
@@ -40,8 +44,39 @@ inline void DrawViewportStatusMessage(const std::string& message) {
drawList->AddText(textPos, ImGui::GetColorU32(ImGuiCol_TextDisabled), message.c_str());
}
inline const char* GetViewportInteractionSurfaceId(EditorViewportKind kind) {
switch (kind) {
case EditorViewportKind::Scene:
return "##SceneViewportInteractionSurface";
case EditorViewportKind::Game:
return "##GameViewportInteractionSurface";
default:
return "##ViewportInteractionSurface";
}
}
inline void RenderViewportInteractionSurface(
ViewportPanelContentResult& result,
EditorViewportKind kind,
const ImVec2& interactionSize) {
ImGui::InvisibleButton(
GetViewportInteractionSurfaceId(kind),
interactionSize,
ImGuiButtonFlags_MouseButtonLeft |
ImGuiButtonFlags_MouseButtonRight |
ImGuiButtonFlags_MouseButtonMiddle);
result.itemMin = ImGui::GetItemRectMin();
result.itemMax = ImGui::GetItemRectMax();
result.hovered = ImGui::IsItemHovered();
result.clickedLeft = ImGui::IsItemClicked(ImGuiMouseButton_Left);
result.clickedRight = ImGui::IsItemClicked(ImGuiMouseButton_Right);
result.clickedMiddle = ImGui::IsItemClicked(ImGuiMouseButton_Middle);
}
inline ViewportPanelContentResult RenderViewportPanelContent(IEditorContext& context, EditorViewportKind kind) {
ViewportPanelContentResult result = {};
UI::CollapsePanelSectionSpacing();
result.availableSize = ImGui::GetContentRegionAvail();
result.focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows);
@@ -54,24 +89,19 @@ inline ViewportPanelContentResult RenderViewportPanelContent(IEditorContext& con
result.hasViewportArea = true;
if (viewportHostService == nullptr) {
ImGui::Dummy(result.availableSize);
result.itemMin = ImGui::GetItemRectMin();
result.itemMax = ImGui::GetItemRectMax();
result.hovered = ImGui::IsMouseHoveringRect(result.itemMin, result.itemMax, true);
RenderViewportInteractionSurface(result, kind, result.availableSize);
DrawViewportStatusMessage("Viewport host is unavailable");
return result;
}
result.frame = viewportHostService->RequestViewport(kind, result.availableSize);
if (result.frame.hasTexture) {
ImGui::Image(result.frame.textureId, result.availableSize);
} else {
ImGui::Dummy(result.availableSize);
}
RenderViewportInteractionSurface(result, kind, result.availableSize);
result.itemMin = ImGui::GetItemRectMin();
result.itemMax = ImGui::GetItemRectMax();
result.hovered = ImGui::IsMouseHoveringRect(result.itemMin, result.itemMax, true);
if (result.frame.hasTexture) {
if (ImDrawList* drawList = ImGui::GetWindowDrawList()) {
drawList->AddImage(result.frame.textureId, result.itemMin, result.itemMax);
}
}
DrawViewportStatusMessage(
result.frame.statusText.empty() && !result.frame.hasTexture