Add scene transform toolbar and scale gizmo
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace XCEngine {
|
||||
@@ -13,9 +14,10 @@ namespace Editor {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float kRotateGizmoAxisRadiusPixels = 84.0f;
|
||||
constexpr float kRotateGizmoViewRadiusPixels = 92.0f;
|
||||
constexpr float kRotateGizmoAxisRadiusPixels = 96.0f;
|
||||
constexpr float kRotateGizmoViewRadiusPixels = 106.0f;
|
||||
constexpr float kRotateGizmoHoverThresholdPixels = 9.0f;
|
||||
constexpr float kRotateGizmoAngleFillMinRadians = 0.01f;
|
||||
|
||||
Math::Vector3 NormalizeVector3(const Math::Vector3& value, const Math::Vector3& fallback) {
|
||||
return value.SqrMagnitude() <= Math::EPSILON ? fallback : value.Normalized();
|
||||
@@ -92,6 +94,12 @@ bool GetRotateRingBasis(
|
||||
}
|
||||
}
|
||||
|
||||
float GetRotateRingRadiusPixels(SceneViewportRotateGizmoAxis axis) {
|
||||
return axis == SceneViewportRotateGizmoAxis::View
|
||||
? kRotateGizmoViewRadiusPixels
|
||||
: kRotateGizmoAxisRadiusPixels;
|
||||
}
|
||||
|
||||
float ComputeWorldUnitsPerPixel(
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector3& worldPoint,
|
||||
@@ -109,20 +117,6 @@ float ComputeWorldUnitsPerPixel(
|
||||
return 2.0f * depth * std::tan(overlay.verticalFovDegrees * Math::DEG_TO_RAD * 0.5f) / viewportHeight;
|
||||
}
|
||||
|
||||
float SignedAngleRadiansAroundAxis(
|
||||
const Math::Vector3& from,
|
||||
const Math::Vector3& to,
|
||||
const Math::Vector3& axis) {
|
||||
const Math::Vector3 normalizedFrom = from.Normalized();
|
||||
const Math::Vector3 normalizedTo = to.Normalized();
|
||||
const float dot = std::clamp(
|
||||
Math::Vector3::Dot(normalizedFrom, normalizedTo),
|
||||
-1.0f,
|
||||
1.0f);
|
||||
const float sine = Math::Vector3::Dot(axis.Normalized(), Math::Vector3::Cross(normalizedFrom, normalizedTo));
|
||||
return std::atan2(sine, dot);
|
||||
}
|
||||
|
||||
float NormalizeSignedAngleRadians(float radians) {
|
||||
while (radians > Math::PI) {
|
||||
radians -= Math::PI * 2.0f;
|
||||
@@ -150,6 +144,28 @@ SceneViewportRotateGizmoAxis GetRotateAxisForIndex(size_t index) {
|
||||
}
|
||||
}
|
||||
|
||||
bool TryComputeRingAngleFromWorldDirection(
|
||||
SceneViewportRotateGizmoAxis axis,
|
||||
const SceneViewportOverlayData& overlay,
|
||||
const Math::Vector3& directionWorld,
|
||||
float& outAngle) {
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(axis, overlay, basisA, basisB)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Math::Vector3 direction = directionWorld.Normalized();
|
||||
const float projectedX = Math::Vector3::Dot(direction, basisA);
|
||||
const float projectedY = Math::Vector3::Dot(direction, basisB);
|
||||
if (projectedX * projectedX + projectedY * projectedY <= Math::EPSILON) {
|
||||
return false;
|
||||
}
|
||||
|
||||
outAngle = std::atan2(projectedY, projectedX);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void SceneViewportRotateGizmo::Update(const SceneViewportRotateGizmoContext& context) {
|
||||
@@ -201,9 +217,18 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
}
|
||||
|
||||
float startRingAngle = 0.0f;
|
||||
if (useScreenSpaceDrag &&
|
||||
!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) {
|
||||
return false;
|
||||
if (useScreenSpaceDrag) {
|
||||
if (!TryGetClosestRingAngle(m_hoveredAxis, context.mousePosition, false, startRingAngle)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (!TryComputeRingAngleFromWorldDirection(
|
||||
m_hoveredAxis,
|
||||
context.overlay,
|
||||
startDirection,
|
||||
startRingAngle)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
undoManager.BeginInteractiveChange("Rotate Gizmo");
|
||||
@@ -217,8 +242,8 @@ bool SceneViewportRotateGizmo::TryBeginDrag(const SceneViewportRotateGizmoContex
|
||||
m_screenSpaceDrag = useScreenSpaceDrag;
|
||||
m_dragPlane = dragPlane;
|
||||
m_dragStartWorldRotation = context.selectedObject->GetTransform()->GetRotation();
|
||||
m_dragStartDirectionWorld = useScreenSpaceDrag ? Math::Vector3::Zero() : startDirection.Normalized();
|
||||
m_dragStartRingAngle = useScreenSpaceDrag ? startRingAngle : 0.0f;
|
||||
m_dragStartRingAngle = startRingAngle;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
RefreshHandleState();
|
||||
return true;
|
||||
}
|
||||
@@ -230,14 +255,11 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
return;
|
||||
}
|
||||
|
||||
float deltaRadians = 0.0f;
|
||||
float currentRingAngle = 0.0f;
|
||||
if (m_screenSpaceDrag) {
|
||||
float currentRingAngle = 0.0f;
|
||||
if (!TryGetClosestRingAngle(m_activeAxis, context.mousePosition, false, currentRingAngle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
|
||||
} else {
|
||||
Math::Ray worldRay;
|
||||
if (!BuildSceneViewportRay(
|
||||
@@ -260,14 +282,22 @@ void SceneViewportRotateGizmo::UpdateDrag(const SceneViewportRotateGizmoContext&
|
||||
return;
|
||||
}
|
||||
|
||||
deltaRadians = SignedAngleRadiansAroundAxis(
|
||||
m_dragStartDirectionWorld,
|
||||
currentDirection,
|
||||
m_activeWorldAxis);
|
||||
if (!TryComputeRingAngleFromWorldDirection(
|
||||
m_activeAxis,
|
||||
context.overlay,
|
||||
currentDirection,
|
||||
currentRingAngle)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const float deltaRadians = NormalizeSignedAngleRadians(currentRingAngle - m_dragStartRingAngle);
|
||||
m_dragCurrentDeltaRadians = deltaRadians;
|
||||
const Math::Quaternion deltaRotation = Math::Quaternion::FromAxisAngle(m_activeWorldAxis, deltaRadians);
|
||||
context.selectedObject->GetTransform()->SetRotation(deltaRotation * m_dragStartWorldRotation);
|
||||
BuildDrawData(context);
|
||||
m_hoveredAxis = m_activeAxis;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
@@ -284,8 +314,8 @@ void SceneViewportRotateGizmo::EndDrag(IUndoManager& undoManager) {
|
||||
m_screenSpaceDrag = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartDirectionWorld = Math::Vector3::Zero();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
RefreshHandleState();
|
||||
}
|
||||
|
||||
@@ -299,8 +329,8 @@ void SceneViewportRotateGizmo::CancelDrag(IUndoManager* undoManager) {
|
||||
m_screenSpaceDrag = false;
|
||||
m_activeWorldAxis = Math::Vector3::Zero();
|
||||
m_dragStartWorldRotation = Math::Quaternion::Identity();
|
||||
m_dragStartDirectionWorld = Math::Vector3::Zero();
|
||||
m_dragStartRingAngle = 0.0f;
|
||||
m_dragCurrentDeltaRadians = 0.0f;
|
||||
m_hoveredAxis = SceneViewportRotateGizmoAxis::None;
|
||||
RefreshHandleState();
|
||||
}
|
||||
@@ -352,21 +382,29 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
|
||||
m_drawData.visible = true;
|
||||
m_drawData.pivot = projectedPivot.screenPosition;
|
||||
const bool hasActiveDragFeedback =
|
||||
m_activeAxis != SceneViewportRotateGizmoAxis::None &&
|
||||
m_activeAxis != SceneViewportRotateGizmoAxis::View &&
|
||||
std::abs(m_dragCurrentDeltaRadians) > Math::EPSILON;
|
||||
const Math::Quaternion dragFeedbackRotation = hasActiveDragFeedback
|
||||
? Math::Quaternion::FromAxisAngle(m_activeWorldAxis, m_dragCurrentDeltaRadians)
|
||||
: Math::Quaternion::Identity();
|
||||
|
||||
for (size_t handleIndex = 0; handleIndex < m_drawData.handles.size(); ++handleIndex) {
|
||||
SceneViewportRotateGizmoHandleDrawData& handle = m_drawData.handles[handleIndex];
|
||||
handle.axis = GetRotateAxisForIndex(handleIndex);
|
||||
handle.color = GetRotateAxisBaseColor(handle.axis);
|
||||
const float ringRadiusWorld = worldUnitsPerPixel *
|
||||
(handle.axis == SceneViewportRotateGizmoAxis::View
|
||||
? kRotateGizmoViewRadiusPixels
|
||||
: kRotateGizmoAxisRadiusPixels);
|
||||
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(handle.axis);
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (!GetRotateRingBasis(handle.axis, context.overlay, basisA, basisB)) {
|
||||
continue;
|
||||
}
|
||||
if (hasActiveDragFeedback && handle.axis != SceneViewportRotateGizmoAxis::View) {
|
||||
basisA = dragFeedbackRotation * basisA;
|
||||
basisB = dragFeedbackRotation * basisB;
|
||||
}
|
||||
|
||||
bool anyVisibleSegment = false;
|
||||
for (size_t segmentIndex = 0; segmentIndex < handle.segments.size(); ++segmentIndex) {
|
||||
@@ -419,6 +457,52 @@ void SceneViewportRotateGizmo::BuildDrawData(const SceneViewportRotateGizmoConte
|
||||
|
||||
handle.visible = anyVisibleSegment;
|
||||
}
|
||||
|
||||
if (m_activeAxis != SceneViewportRotateGizmoAxis::None &&
|
||||
std::abs(m_dragCurrentDeltaRadians) >= kRotateGizmoAngleFillMinRadians) {
|
||||
SceneViewportRotateGizmoAngleFillDrawData& angleFill = m_drawData.angleFill;
|
||||
angleFill.axis = m_activeAxis;
|
||||
angleFill.pivot = projectedPivot.screenPosition;
|
||||
angleFill.fillColor = Math::Color(1.0f, 0.92f, 0.12f, 0.22f);
|
||||
angleFill.outlineColor = Math::Color(1.0f, 0.92f, 0.12f, 0.95f);
|
||||
|
||||
Math::Vector3 basisA = Math::Vector3::Zero();
|
||||
Math::Vector3 basisB = Math::Vector3::Zero();
|
||||
if (GetRotateRingBasis(m_activeAxis, context.overlay, basisA, basisB)) {
|
||||
const float ringRadiusWorld = worldUnitsPerPixel * GetRotateRingRadiusPixels(m_activeAxis);
|
||||
const float sweepRadians = NormalizeSignedAngleRadians(m_dragCurrentDeltaRadians);
|
||||
const float sweepAbs = std::abs(sweepRadians);
|
||||
const size_t stepCount = std::clamp(
|
||||
static_cast<size_t>(std::ceil(
|
||||
sweepAbs / (Math::PI * 2.0f) * static_cast<float>(kSceneViewportRotateGizmoSegmentCount))),
|
||||
static_cast<size_t>(1),
|
||||
kSceneViewportRotateGizmoAngleFillPointCount - 1);
|
||||
|
||||
bool valid = true;
|
||||
for (size_t pointIndex = 0; pointIndex <= stepCount; ++pointIndex) {
|
||||
const float t = static_cast<float>(pointIndex) / static_cast<float>(stepCount);
|
||||
const float angle = m_dragStartRingAngle + sweepRadians * t;
|
||||
const Math::Vector3 worldPoint =
|
||||
pivotWorldPosition + (basisA * std::cos(angle) + basisB * std::sin(angle)) * ringRadiusWorld;
|
||||
const SceneViewportProjectedPoint projectedPoint = ProjectSceneViewportWorldPoint(
|
||||
context.overlay,
|
||||
context.viewportSize.x,
|
||||
context.viewportSize.y,
|
||||
worldPoint);
|
||||
if (projectedPoint.ndcDepth < 0.0f || projectedPoint.ndcDepth > 1.0f) {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
|
||||
angleFill.arcPoints[pointIndex] = projectedPoint.screenPosition;
|
||||
}
|
||||
|
||||
if (valid) {
|
||||
angleFill.arcPointCount = stepCount + 1;
|
||||
angleFill.visible = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneViewportRotateGizmo::RefreshHandleState() {
|
||||
|
||||
Reference in New Issue
Block a user