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

475 lines
13 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "SpotLightGizmo.h"
#include "Components/SpotLightComponent.h"
#include "ContextObjectStore.h"
#include "Engine/SpotLight.h"
#include "ScalableConeGizmo.h"
#include "InteractiveGizmoManager.h"
#include "LightGizmosModule.h"
#include "Engine/CollisionProfile.h"
#include "Engine/World.h"
#include "BaseGizmos/GizmoMath.h"
#include "BaseGizmos/GizmoViewContext.h"
#include "Components/SphereComponent.h"
#include "BaseGizmos/GizmoLineHandleComponent.h"
#include "BaseBehaviors/MouseHoverBehavior.h"
#include "InteractiveToolManager.h"
#include "SubTransformProxy.h"
#include "ToolContextInterfaces.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(SpotLightGizmo)
#define LOCTEXT_NAMESPACE "USpotLightGizmo"
// USpotLightGizmoBuilder
UInteractiveGizmo* USpotLightGizmoBuilder::BuildGizmo(const FToolBuilderState& SceneState) const
{
USpotLightGizmo* NewGizmo = NewObject<USpotLightGizmo>(SceneState.GizmoManager);
NewGizmo->SetWorld(SceneState.World);
UGizmoViewContext* GizmoViewContext = SceneState.ToolManager->GetContextObjectStore()->FindContext<UGizmoViewContext>();
check(GizmoViewContext && GizmoViewContext->IsValidLowLevel());
NewGizmo->SetGizmoViewContext(GizmoViewContext);
return NewGizmo;
}
// ASpotLightGizmoActor
ASpotLightGizmoActor::ASpotLightGizmoActor()
{
// 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);
}
// USpotLightGizmo
USpotLightGizmo::USpotLightGizmo()
{
LightActor = nullptr;
TransformProxy = nullptr;
}
void USpotLightGizmo::Setup()
{
USpotLightGizmoInputBehavior* SpotLightBehavior = NewObject<USpotLightGizmoInputBehavior>(this);
SpotLightBehavior->Initialize(this);
AddInputBehavior(SpotLightBehavior);
UMouseHoverBehavior* HoverBehavior = NewObject<UMouseHoverBehavior>(this);
HoverBehavior->Initialize(this);
AddInputBehavior(HoverBehavior);
}
void USpotLightGizmo::Tick(float DeltaTime)
{
// Make sure the gizmos are up to date with the various light properties
if (OuterAngleGizmo)
{
OuterAngleGizmo->SetAngleDegrees(LightActor->SpotLightComponent->OuterConeAngle);
OuterAngleGizmo->SetLength(LightActor->SpotLightComponent->AttenuationRadius);
}
if (InnerAngleGizmo)
{
InnerAngleGizmo->SetAngleDegrees(LightActor->SpotLightComponent->InnerConeAngle);
InnerAngleGizmo->SetLength(LightActor->SpotLightComponent->AttenuationRadius);
}
if (GizmoActor)
{
GizmoActor->AttenuationScaleHandle->SetRelativeLocation(FVector(LightActor->SpotLightComponent->AttenuationRadius, 0, 0));
}
}
void USpotLightGizmo::Shutdown()
{
if (OuterAngleGizmo)
{
GetGizmoManager()->DestroyGizmo(OuterAngleGizmo);
OuterAngleGizmo = nullptr;
}
if (InnerAngleGizmo)
{
GetGizmoManager()->DestroyGizmo(InnerAngleGizmo);
InnerAngleGizmo = nullptr;
}
if (GizmoActor)
{
GizmoActor->Destroy();
GizmoActor = nullptr;
}
}
FInputRayHit USpotLightGizmo::BeginHoverSequenceHitTest(const FInputDeviceRay& PressPos)
{
FHitResult HitResult;
FTransform DragTransform;
if (HitTest(PressPos.WorldRay, HitResult, DragTransform))
{
bIsHovering = true;
DragStartWorldPosition = DragTransform.GetLocation();
UpdateHandleColors();
return FInputRayHit(HitResult.Distance);
}
// check to avoid refreshing handle colors constantly even if not required
if (bIsHovering)
{
bIsHovering = false;
UpdateHandleColors();
}
// Return invalid ray hit to say we don't want to listen to hover input
return FInputRayHit();
}
bool USpotLightGizmo::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))
{
bIsHovering = true;
DragStartWorldPosition = DragTransform.GetLocation();
return true;
}
bIsHovering = false;
return false;
}
void USpotLightGizmo::OnEndHover()
{
bIsHovering = false;
UpdateHandleColors();
}
void USpotLightGizmo::UpdateHandleColors()
{
if (bIsHovering || bIsDragging)
{
GizmoActor->AttenuationScaleHandle->Color = FLinearColor::Yellow;
}
else
{
GizmoActor->AttenuationScaleHandle->Color = FLinearColor::Blue;
}
GizmoActor->AttenuationScaleHandle->NotifyExternalPropertyUpdates();
}
void USpotLightGizmo::SetSelectedObject(ASpotLight* InLight)
{
LightActor = InLight;
// TODO: No way to remove a component from the transform proxy
if (!TransformProxy)
{
TransformProxy = NewObject<USubTransformProxy>(this);
}
USceneComponent* SceneComponent = LightActor->GetRootComponent();
TransformProxy->AddComponent(SceneComponent);
}
USubTransformProxy* USpotLightGizmo::GetTransformProxy()
{
return TransformProxy;
}
void USpotLightGizmo::OnOuterAngleUpdate(float NewAngle)
{
LightActor->SpotLightComponent->Modify();
LightActor->SpotLightComponent->OuterConeAngle = NewAngle;
// OuterAngle cannot be less than Inner Angle
if (NewAngle < LightActor->SpotLightComponent->InnerConeAngle)
{
LightActor->SpotLightComponent->InnerConeAngle = NewAngle;
}
LightActor->SpotLightComponent->MarkRenderStateDirty();
}
void USpotLightGizmo::OnInnerAngleUpdate(float NewAngle)
{
LightActor->SpotLightComponent->Modify();
LightActor->SpotLightComponent->InnerConeAngle = NewAngle;
// Inner Angle cannot be greater than Outer Angle
if (NewAngle > LightActor->SpotLightComponent->OuterConeAngle)
{
LightActor->SpotLightComponent->OuterConeAngle = NewAngle;
}
LightActor->SpotLightComponent->MarkRenderStateDirty();
}
void USpotLightGizmo::OnTransformChanged(UTransformProxy*, FTransform)
{
if (!GizmoActor)
{
return;
}
USceneComponent* GizmoComponent = GizmoActor->GetRootComponent();
FTransform TargetTransform = TransformProxy->GetTransform();
// The gizmo doesn't want the scale of the target
TargetTransform.SetScale3D(FVector(1, 1, 1));
GizmoComponent->SetWorldTransform(TargetTransform);
}
void USpotLightGizmo::CreateOuterAngleGizmo()
{
if (!LightActor)
{
return;
}
OuterAngleGizmo = Cast<UScalableConeGizmo>(GetGizmoManager()->CreateGizmo(FLightGizmosModule::ScalableConeGizmoType));
OuterAngleGizmo->SetTarget(TransformProxy);
OuterAngleGizmo->UpdateAngleFunc = [this](float NewAngle) { this->OnOuterAngleUpdate(NewAngle); };
OuterAngleGizmo->MaxAngle = 80.f;
OuterAngleGizmo->MinAngle = 1.f;
OuterAngleGizmo->TransactionDescription = LOCTEXT("SpotLightOuterAngle", "Spot Light Outer Angle");
}
void USpotLightGizmo::CreateInnerAngleGizmo()
{
if (!LightActor)
{
return;
}
InnerAngleGizmo = Cast<UScalableConeGizmo>(GetGizmoManager()->CreateGizmo(FLightGizmosModule::ScalableConeGizmoType));
InnerAngleGizmo->SetTarget(TransformProxy);
InnerAngleGizmo->UpdateAngleFunc = [this](float NewAngle) { this->OnInnerAngleUpdate(NewAngle); };
InnerAngleGizmo->MaxAngle = 80.f;
InnerAngleGizmo->MinAngle = 1.f;
InnerAngleGizmo->ConeColor = FColor(150, 200, 255);
InnerAngleGizmo->TransactionDescription = LOCTEXT("SpotLightInnerAngle", "Spot Light Inner Angle");
}
void USpotLightGizmo::SetWorld(UWorld* InWorld)
{
World = InWorld;
}
void USpotLightGizmo::SetGizmoViewContext(UGizmoViewContext* GizmoViewContextIn)
{
GizmoViewContext = GizmoViewContextIn;
}
void USpotLightGizmo::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;
// Check if any of the components were hit
if (HitTest(HitCheckRay, HitResult, DragTransform))
{
FVector RayNearestPt; float RayNearestParam;
FVector InteractionStartPoint;
// Update interaction start parameters
GizmoMath::NearestPointOnLineToRay(DragTransform.GetLocation(), GizmoActor->GetActorForwardVector(),
Ray.WorldRay.Origin, Ray.WorldRay.Direction,
InteractionStartPoint, InteractionStartParameter,
RayNearestPt, RayNearestParam);
DragStartWorldPosition = DragTransform.GetLocation();
bIsDragging = true;
UpdateHandleColors();
GetGizmoManager()->BeginUndoTransaction(LOCTEXT("SpotLightAttenuation", "Spot Light Attenuation"));
}
}
void USpotLightGizmo::OnUpdateDrag(const FInputDeviceRay& Ray)
{
FVector AxisNearestPt; float AxisNearestParam;
FVector RayNearestPt; float RayNearestParam;
// Get current interaction parameters
GizmoMath::NearestPointOnLineToRay(DragStartWorldPosition, GizmoActor->GetActorForwardVector(),
Ray.WorldRay.Origin, Ray.WorldRay.Direction,
AxisNearestPt, AxisNearestParam,
RayNearestPt, RayNearestParam);
float InteractionCurParameter = AxisNearestParam;
float DeltaParam = InteractionCurParameter - InteractionStartParameter;
InteractionStartParameter = InteractionCurParameter;
// Update the attenuation of the cone
float NewAttenuation = LightActor->SpotLightComponent->AttenuationRadius + DeltaParam;
NewAttenuation = (NewAttenuation < 0) ? 0 : NewAttenuation;
LightActor->SpotLightComponent->Modify();
LightActor->SpotLightComponent->AttenuationRadius = NewAttenuation;
LightActor->SpotLightComponent->MarkRenderStateDirty();
if (OuterAngleGizmo)
{
OuterAngleGizmo->SetLength(NewAttenuation);
}
if (InnerAngleGizmo)
{
InnerAngleGizmo->SetLength(NewAttenuation);
}
}
void USpotLightGizmo::OnEndDrag(const FInputDeviceRay& Ray)
{
bIsDragging = false;
UpdateHandleColors();
GetGizmoManager()->EndUndoTransaction();
}
bool USpotLightGizmo::HitTest(const FRay& Ray, FHitResult& OutHit, FTransform& OutTransform)
{
if (!GizmoActor)
{
return false;
}
FVector Start = Ray.Origin;
const float MaxRaycastDistance = 1e6f;
FVector End = Ray.Origin + Ray.Direction * MaxRaycastDistance;
FCollisionQueryParams Params;
if (GizmoActor->AttenuationScaleHandle->LineTraceComponent(OutHit, Start, End, Params))
{
OutTransform = GizmoActor->AttenuationScaleHandle->GetComponentTransform();
return true;
}
return false;
}
void USpotLightGizmo::CreateAttenuationScaleGizmo()
{
if (!OuterAngleGizmo)
{
return;
}
FActorSpawnParameters SpawnInfo;
GizmoActor = World->SpawnActor<ASpotLightGizmoActor>(FVector::ZeroVector, FRotator::ZeroRotator, SpawnInfo);
// The handle to scale attenuation is line handle component
GizmoActor->AttenuationScaleHandle = AGizmoActor::AddDefaultLineHandleComponent(World, GizmoActor, GizmoViewContext,
FLinearColor::Blue, FVector::YAxisVector, FVector::XAxisVector, 60.f, true);
GizmoActor->AttenuationScaleHandle->SetRelativeLocation(FVector(LightActor->SpotLightComponent->AttenuationRadius, 0, 0));
TransformProxy->OnTransformChanged.AddUObject(this, &USpotLightGizmo::OnTransformChanged);
OnTransformChanged(TransformProxy, TransformProxy->GetTransform());
}
// USpotLightGizmoInputBehavior
void USpotLightGizmoInputBehavior::Initialize(USpotLightGizmo* InGizmo)
{
Gizmo = InGizmo;
}
FInputCaptureRequest USpotLightGizmoInputBehavior::WantsCapture(const FInputDeviceState& input)
{
if (IsPressed(input))
{
FHitResult HitResult;
FTransform DragTransform;
if (Gizmo->HitTest(input.Mouse.WorldRay, HitResult, DragTransform))
{
return FInputCaptureRequest::Begin(this, EInputCaptureSide::Any, HitResult.Distance);
}
}
return FInputCaptureRequest::Ignore();
}
FInputCaptureUpdate USpotLightGizmoInputBehavior::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 USpotLightGizmoInputBehavior::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 USpotLightGizmoInputBehavior::ForceEndCapture(const FInputCaptureData& data)
{
if (bInputDragCaptured)
{
Gizmo->OnEndDrag(FInputDeviceRay(LastWorldRay));
bInputDragCaptured = false;
}
}
#undef LOCTEXT_NAMESPACE