Files
2025-05-18 13:04:45 +08:00

504 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DirectionalLightGizmo.h"
#include "BaseGizmos/GizmoViewContext.h"
#include "Components/SphereComponent.h"
#include "ContextObjectStore.h"
#include "Engine/CollisionProfile.h"
#include "Engine/DirectionalLight.h"
#include "Engine/World.h"
#include "BaseGizmos/GizmoMath.h"
#include "BaseGizmos/GizmoCircleComponent.h"
#include "BaseGizmos/GizmoLineHandleComponent.h"
#include "BaseGizmos/GizmoRenderingUtil.h"
#include "InteractiveGizmoManager.h"
#include "PrimitiveDrawingUtils.h"
#include "BaseBehaviors/MouseHoverBehavior.h"
#include "InteractiveToolManager.h"
#include "SubTransformProxy.h"
#include "ToolContextInterfaces.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(DirectionalLightGizmo)
#define LOCTEXT_NAMESPACE "UDirectionalLightGizmo"
// UDirectionalLightGizmoBuilder
UInteractiveGizmo* UDirectionalLightGizmoBuilder::BuildGizmo(const FToolBuilderState& SceneState) const
{
UDirectionalLightGizmo* NewGizmo = NewObject<UDirectionalLightGizmo>(SceneState.GizmoManager);
NewGizmo->SetWorld(SceneState.World);
UGizmoViewContext* GizmoViewContext = SceneState.ToolManager->GetContextObjectStore()->FindContext<UGizmoViewContext>();
check(GizmoViewContext && GizmoViewContext->IsValidLowLevel());
NewGizmo->SetGizmoViewContext(GizmoViewContext);
return NewGizmo;
}
// ADirectionalLightGizmoActor
ADirectionalLightGizmoActor::ADirectionalLightGizmoActor()
{
// root component is a hidden sphere
USphereComponent* SphereComponent = CreateDefaultSubobject<USphereComponent>(TEXT("GizmoCenter"));
RootComponent = SphereComponent;
SphereComponent->InitSphereRadius(1.0f);
SphereComponent->SetVisibility(false);
SphereComponent->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName);
}
// UDirectionalLightGizmo
UDirectionalLightGizmo::UDirectionalLightGizmo()
{
LightActor = nullptr;
GizmoActor = nullptr;
TransformProxy = nullptr;
}
void UDirectionalLightGizmo::Setup()
{
UInteractiveGizmo::Setup();
UDirectionalLightGizmoInputBehavior* DirectionalLightBehavior = NewObject<UDirectionalLightGizmoInputBehavior>(this);
DirectionalLightBehavior->Initialize(this);
AddInputBehavior(DirectionalLightBehavior);
UMouseHoverBehavior* HoverBehavior = NewObject<UMouseHoverBehavior>(this);
HoverBehavior->Initialize(this);
AddInputBehavior(HoverBehavior);
CreateGizmoHandles();
CreateZRotationGizmo();
// The Gizmo is being rotated around the Y axis
RotationPlaneX = FVector::XAxisVector;
RotationPlaneZ = FVector::ZAxisVector;
}
void UDirectionalLightGizmo::Render(IToolsContextRenderAPI* RenderAPI)
{
FVector Start = GizmoActor->GetActorLocation();
// Parameters of the handle to rotate around y axis
FVector ArrowDir = GizmoActor->GetActorRotation().Vector();
FVector ArrowEnd = Start + ArrowDir * ArrowLength;
FVector ZAxis;
// Checking if the rotation normal is positive or negative Z axis
if (ArrowEnd.Z < Start.Z)
{
ZAxis = -FVector::ZAxisVector;
}
else
{
ZAxis = FVector::ZAxisVector;
}
// Project the arrow end onto the XY plane to get it's direction along that plane
FVector EndProjection = GizmoMath::ProjectPointOntoPlane(ArrowEnd, Start, ZAxis);
FVector LineDir = EndProjection - Start;
LineDir.Normalize();
// The end point of the line
FVector LineEnd = Start + LineDir * ArrowLength;
const FSceneView* View = RenderAPI->GetSceneView();
float PixelToWorld = UE::GizmoRenderingUtil::CalculateLocalPixelToWorldScale(View, LineEnd);
// Calculate the "true" end point in world space (We want the line to be the same length in screen space)
FVector LineEndWorld = Start + LineDir * ArrowLength * PixelToWorld;
// Draw the line that represents the arrow projected onto the circle
RenderAPI->GetPrimitiveDrawInterface()->DrawLine(Start, LineEndWorld, FLinearColor::Red, SDPG_Foreground);
// Figure out the angle of the arc between the line and the arrow
float ArcAngle = FMath::Acos(static_cast<float>(FVector::DotProduct(LineDir, ArrowDir)));
ArcAngle = FMath::RadiansToDegrees(ArcAngle);
// Draw an arc between the arrow and the line
DrawArc(RenderAPI->GetPrimitiveDrawInterface(), Start, LineDir, ZAxis, 0, ArcAngle, ArrowLength * PixelToWorld, 32, FLinearColor(0.f, 0.f, 1.f), SDPG_Foreground);
}
void UDirectionalLightGizmo::Shutdown()
{
if (GizmoActor)
{
GizmoActor->Destroy();
GizmoActor = nullptr;
}
}
USubTransformProxy* UDirectionalLightGizmo::GetTransformProxy()
{
return TransformProxy;
}
FInputRayHit UDirectionalLightGizmo::BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos)
{
FHitResult HitResult;
FTransform DragTransform;
if (HitTest(PressPos.WorldRay, HitResult, DragTransform, HitComponent))
{
bIsHovering = true;
UpdateHandleColors();
return FInputRayHit(HitResult.Distance);
}
bIsHovering = false;
// Return invalid ray hit to say we don't want to listen to hover input
return FInputRayHit();
}
bool UDirectionalLightGizmo::OnUpdateHover(const FInputDeviceRay& DevicePos)
{
if (!LightActor)
{
bIsHovering = false;
return false;
}
FVector Start = DevicePos.WorldRay.Origin;
const float MaxRaycastDistance = 1e6f;
FVector End = DevicePos.WorldRay.Origin + DevicePos.WorldRay.Direction * MaxRaycastDistance;
FRay HitCheckRay(Start, End - Start);
FHitResult HitResult;
FTransform DragTransform;
if (HitTest(HitCheckRay, HitResult, DragTransform, HitComponent))
{
bIsHovering = true;
return true;
}
bIsHovering = false;
return false;
}
void UDirectionalLightGizmo::OnEndHover()
{
bIsHovering = false;
UpdateHandleColors();
}
void UDirectionalLightGizmo::SetSelectedObject(ADirectionalLight* InLight)
{
LightActor = InLight;
// TODO: Cannot remove a component from Transform Proxy
if (!TransformProxy)
{
TransformProxy = NewObject<USubTransformProxy>(this);
}
USceneComponent* SceneComponent = LightActor->GetRootComponent();
TransformProxy->AddComponent(SceneComponent);
TransformProxy->OnTransformChanged.AddUObject(this, &UDirectionalLightGizmo::OnTransformChanged);
OnTransformChanged(TransformProxy, TransformProxy->GetTransform());
}
void UDirectionalLightGizmo::SetWorld(UWorld* InWorld)
{
World = InWorld;
}
void UDirectionalLightGizmo::SetGizmoViewContext(UGizmoViewContext* GizmoViewContextIn)
{
GizmoViewContext = GizmoViewContextIn;
}
void UDirectionalLightGizmo::OnBeginDrag(const FInputDeviceRay& Ray)
{
FVector Start = Ray.WorldRay.Origin;
const float MaxRaycastDistance = 1e6f;
FVector End = Ray.WorldRay.Origin + Ray.WorldRay.Direction * MaxRaycastDistance;
FRay HitCheckRay(Start, End - Start);
FHitResult HitResult;
FTransform DragTransform;
HitComponent = nullptr;
// Check if any component was hit
if (HitTest(HitCheckRay, HitResult, DragTransform, HitComponent))
{
bIsDragging = true;
UpdateHandleColors();
// Rotate around y axis if the arrow was hit
if (HitComponent == GizmoActor->Arrow)
{
HitAxis = LightActor->GetActorRotation().RotateVector(FVector::YAxisVector);
// Get the rotated plane vectors for the interaction
GizmoMath::MakeNormalPlaneBasis(HitAxis, RotationPlaneX, RotationPlaneZ);
RotationPlaneX = LightActor->GetActorRotation().RotateVector(FVector::XAxisVector);
RotationPlaneZ = LightActor->GetActorRotation().RotateVector(FVector::ZAxisVector);
GetGizmoManager()->BeginUndoTransaction(LOCTEXT("DirectionalLightYRotation", "Directional Light Y Rotation"));
}
// Rotate around Z axis if the circle was hit
else
{
HitAxis = FVector::ZAxisVector;
RotationPlaneX = FVector::XAxisVector;
RotationPlaneZ = FVector::YAxisVector;
GetGizmoManager()->BeginUndoTransaction(LOCTEXT("DirectionalLightZRotation", "Directional Light Z Rotation"));
}
// Calculate initial hit position
DragStartWorldPosition = GizmoMath::ProjectPointOntoLine(Ray.WorldRay.PointAt(HitResult.Distance), DragTransform.GetLocation(), HitAxis);
// Calculate initial hit parameters
bool bIntersects;
FVector IntersectionPoint;
GizmoMath::RayPlaneIntersectionPoint(
DragStartWorldPosition, HitAxis,
Ray.WorldRay.Origin, Ray.WorldRay.Direction,
bIntersects, IntersectionPoint);
check(bIntersects); // need to handle this case...
InteractionStartPoint = IntersectionPoint;
InteractionStartParameter = GizmoMath::ComputeAngleInPlane(InteractionStartPoint,
DragStartWorldPosition, HitAxis, RotationPlaneX, RotationPlaneZ);
}
}
void UDirectionalLightGizmo::OnUpdateDrag(const FInputDeviceRay& Ray)
{
bool bIntersects;
FVector IntersectionPoint;
// Calculate current hit parameters
GizmoMath::RayPlaneIntersectionPoint(
DragStartWorldPosition, HitAxis,
Ray.WorldRay.Origin, Ray.WorldRay.Direction,
bIntersects, IntersectionPoint);
if (!bIntersects)
{
return;
}
FVector InteractionCurPoint = IntersectionPoint;
float InteractionCurAngle = GizmoMath::ComputeAngleInPlane(InteractionCurPoint,
DragStartWorldPosition, HitAxis, RotationPlaneX, RotationPlaneZ);
float DeltaAngle = InteractionCurAngle - InteractionStartParameter;
LightActor->Modify();
// Rotate around y axis if the arrow was hit
if (HitComponent == GizmoActor->Arrow)
{
FRotator Rotation = FRotator::ZeroRotator;
Rotation.Pitch = FMath::RadiansToDegrees(DeltaAngle);
LightActor->AddActorLocalRotation(Rotation);
}
// Rotate around Z axis if the circle was hit
else
{
FQuat Rotation(FVector(0, 0, 1), DeltaAngle);
LightActor->AddActorWorldRotation(Rotation);
}
TransformProxy->SetTransform(LightActor->GetTransform());
InteractionStartPoint = InteractionCurPoint;
InteractionStartParameter = InteractionCurAngle;
}
void UDirectionalLightGizmo::OnEndDrag(const FInputDeviceRay& Ray)
{
bIsDragging = false;
UpdateHandleColors();
GetGizmoManager()->EndUndoTransaction();
}
template<typename PtrType>
bool UDirectionalLightGizmo::HitTest(const FRay& Ray, FHitResult& OutHit, FTransform& OutTransform, PtrType& OutHitComponent)
{
FVector Start = Ray.Origin;
const float MaxRaycastDistance = 1e6f;
FVector End = Ray.Origin + Ray.Direction * MaxRaycastDistance;
FCollisionQueryParams Params;
if (GizmoActor->Arrow->LineTraceComponent(OutHit, Start, End, Params))
{
OutTransform = GizmoActor->Arrow->GetComponentTransform();
OutHitComponent = GizmoActor->Arrow;
return true;
}
else if (GizmoActor->RotationZCircle && GizmoActor->RotationZCircle->LineTraceComponent(OutHit, Start, End, Params))
{
OutTransform = GizmoActor->GetTransform();
OutHitComponent = GizmoActor->RotationZCircle;
return true;
}
return false;
}
void UDirectionalLightGizmo::CreateGizmoHandles()
{
FActorSpawnParameters SpawnInfo;
GizmoActor = World->SpawnActor<ADirectionalLightGizmoActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo);
GizmoActor->Arrow = AGizmoActor::AddDefaultLineHandleComponent(World, GizmoActor, GizmoViewContext,
FLinearColor::Red, FVector::YAxisVector, FVector::XAxisVector, ArrowLength, true);
}
void UDirectionalLightGizmo::UpdateGizmoHandles()
{
if (GizmoActor && GizmoActor->RotationZCircle)
{
GizmoActor->RotationZCircle->SetRelativeRotation(GizmoActor->GetActorRotation().Quaternion().Inverse());
}
}
void UDirectionalLightGizmo::UpdateHandleColors()
{
if (bIsHovering || bIsDragging)
{
if (HitComponent)
{
HitComponent->Color = FLinearColor::Yellow;
HitComponent->NotifyExternalPropertyUpdates();
}
}
else
{
GizmoActor->Arrow->Color = FLinearColor::Red;
GizmoActor->Arrow->NotifyExternalPropertyUpdates();
GizmoActor->RotationZCircle->Color = FLinearColor::Blue;
GizmoActor->RotationZCircle->NotifyExternalPropertyUpdates();
}
}
void UDirectionalLightGizmo::OnTransformChanged(UTransformProxy*, FTransform)
{
if (!GizmoActor)
{
return;
}
USceneComponent* GizmoComponent = GizmoActor->GetRootComponent();
FTransform TargetTransform = TransformProxy->GetTransform();
FTransform GizmoTransform = TargetTransform;
GizmoTransform.SetScale3D(FVector(1, 1, 1));
GizmoComponent->SetWorldTransform(GizmoTransform);
UpdateGizmoHandles();
}
void UDirectionalLightGizmo::CreateZRotationGizmo()
{
UGizmoCircleComponent* NewCircle = NewObject<UGizmoCircleComponent>(GizmoActor);
GizmoActor->AddInstanceComponent(NewCircle);
NewCircle->AttachToComponent(GizmoActor->GetRootComponent(), FAttachmentTransformRules::KeepRelativeTransform);
NewCircle->Normal = FVector::ZAxisVector;
NewCircle->Color = FLinearColor::Blue;
NewCircle->Radius = 120.f;
NewCircle->bDrawFullCircle = true;
NewCircle->RegisterComponent();
GizmoActor->RotationZCircle = NewCircle;
UpdateGizmoHandles();
}
// UDirectionalLightGizmoInputBehavior
void UDirectionalLightGizmoInputBehavior::Initialize(UDirectionalLightGizmo* InGizmo)
{
Gizmo = InGizmo;
}
FInputCaptureRequest UDirectionalLightGizmoInputBehavior::WantsCapture(const FInputDeviceState& input)
{
if (IsPressed(input))
{
FHitResult HitResult;
FTransform DragTransform;
UGizmoBaseComponent* HitComponent = nullptr;
if (Gizmo->HitTest(input.Mouse.WorldRay, HitResult, DragTransform, HitComponent))
{
return FInputCaptureRequest::Begin(this, EInputCaptureSide::Any, HitResult.Distance);
}
}
return FInputCaptureRequest::Ignore();
}
FInputCaptureUpdate UDirectionalLightGizmoInputBehavior::BeginCapture(const FInputDeviceState& input, EInputCaptureSide eSide)
{
FInputDeviceRay DeviceRay(input.Mouse.WorldRay, input.Mouse.Position2D);
LastWorldRay = DeviceRay.WorldRay;
LastScreenPosition = DeviceRay.ScreenPosition;
Gizmo->OnBeginDrag(DeviceRay);
bInputDragCaptured = true;
return FInputCaptureUpdate::Begin(this, EInputCaptureSide::Any);
}
FInputCaptureUpdate UDirectionalLightGizmoInputBehavior::UpdateCapture(const FInputDeviceState& input, const FInputCaptureData& data)
{
FInputDeviceRay DeviceRay(input.Mouse.WorldRay, input.Mouse.Position2D);
LastWorldRay = DeviceRay.WorldRay;
LastScreenPosition = DeviceRay.ScreenPosition;
if (IsReleased(input))
{
bInputDragCaptured = false;
Gizmo->OnEndDrag(FInputDeviceRay(LastWorldRay));
return FInputCaptureUpdate::End();
}
Gizmo->OnUpdateDrag(FInputDeviceRay(LastWorldRay));
return FInputCaptureUpdate::Continue();
}
void UDirectionalLightGizmoInputBehavior::ForceEndCapture(const FInputCaptureData& data)
{
if (bInputDragCaptured)
{
Gizmo->OnEndDrag(FInputDeviceRay(LastWorldRay));
bInputDragCaptured = false;
}
}
#undef LOCTEXT_NAMESPACE