Files
UnrealEngine/Engine/Plugins/Runtime/nDisplay/Source/DisplayClusterScenePreview/Private/DisplayClusterLightCardEditorHelper.cpp
2025-05-18 13:04:45 +08:00

1598 lines
53 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "DisplayClusterLightCardEditorHelper.h"
#include "IDisplayClusterScenePreview.h"
#include "DisplayClusterConfigurationTypes.h"
#include "DisplayClusterChromakeyCardActor.h"
#include "DisplayClusterLightCardActor.h"
#include "DisplayClusterRootActor.h"
#include "DisplayClusterRootActorContainers.h"
#include "Components/DisplayClusterCameraComponent.h"
#include "StageActor/DisplayClusterStageActorTemplate.h"
#include "CanvasTypes.h"
#include "KismetProceduralMeshLibrary.h"
#include "PreviewScene.h"
#include "ProceduralMeshComponent.h"
#include "Components/DisplayClusterStageGeometryComponent.h"
#include "Containers/ArrayView.h"
#include "Engine/Blueprint.h"
#include "Engine/StaticMesh.h"
#include "Engine/Texture2D.h"
#include "UObject/Package.h"
#include "UObject/UObjectGlobals.h"
#if WITH_EDITOR
#include "EditorViewportClient.h"
#endif
//////////////////////////////////////////////////////////////////////////
// FSphericalCoordinates
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::FSphericalCoordinates(const FVector& CartesianPosition)
{
Radius = CartesianPosition.Size();
if (Radius > UE_SMALL_NUMBER)
{
Inclination = FMath::Acos(CartesianPosition.Z / Radius);
}
else
{
Inclination = 0;
}
Azimuth = FMath::Atan2(CartesianPosition.Y, CartesianPosition.X);
}
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::FSphericalCoordinates()
: Radius(0)
, Inclination(0)
, Azimuth(0)
{
}
FVector FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::AsCartesian() const
{
const double SinAzimuth = FMath::Sin(Azimuth);
const double CosAzimuth = FMath::Cos(Azimuth);
const double SinInclination = FMath::Sin(Inclination);
const double CosInclination = FMath::Cos(Inclination);
return FVector(
Radius * CosAzimuth * SinInclination,
Radius * SinAzimuth * SinInclination,
Radius * CosInclination
);
}
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::operator+(FSphericalCoordinates const& Other) const
{
FSphericalCoordinates Result;
Result.Radius = Radius + Other.Radius;
Result.Inclination = Inclination + Other.Inclination;
Result.Azimuth = Azimuth + Other.Azimuth;
return Result;
}
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::operator-(FSphericalCoordinates const& Other) const
{
FSphericalCoordinates Result;
Result.Radius = Radius - Other.Radius;
Result.Inclination = Inclination - Other.Inclination;
Result.Azimuth = Azimuth - Other.Azimuth;
return Result;
}
void FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::Conform()
{
if (Radius < 0)
{
Radius = -Radius;
Inclination += PI;
}
if (Inclination < 0 || Inclination > PI)
{
// -2PI to 2PI
Inclination = FMath::Fmod(Inclination, 2 * PI);
// 0 to 2PI
if (Inclination < 0)
{
Inclination += 2 * PI;
}
// 0 to PI
if (Inclination > PI)
{
Inclination = 2 * PI - Inclination;
Azimuth += PI;
}
}
if (Azimuth < -PI || Azimuth > PI)
{
// -2PI to 2PI
Azimuth = FMath::Fmod(Azimuth, 2 * PI);
// -PI to PI
if (Azimuth > PI)
{
Azimuth -= 2 * PI;
}
else if (Azimuth < -PI)
{
Azimuth += 2 * PI;
}
}
checkSlow(Radius >= 0);
checkSlow(Inclination >= 0 && Inclination <= PI);
checkSlow(Azimuth >= -PI && Azimuth <= PI);
}
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::GetConformed() const
{
FSphericalCoordinates Result = *this;
Result.Conform();
return Result;
}
bool FDisplayClusterLightCardEditorHelper::FSphericalCoordinates::IsPointingAtPole(double Margin) const
{
FSphericalCoordinates CoordsConformed = GetConformed();
return FMath::IsNearlyZero(CoordsConformed.Inclination, Margin)
|| FMath::IsNearlyEqual(CoordsConformed.Inclination, PI, Margin);
}
//////////////////////////////////////////////////////////////////////////
// FDisplayClusterLightCardEditorHelper
FDisplayClusterLightCardEditorHelper::FDisplayClusterLightCardEditorHelper()
: FDisplayClusterLightCardEditorHelper(IDisplayClusterScenePreview::Get().CreateRenderer())
{
bCreatedRenderer = true;
}
FDisplayClusterLightCardEditorHelper::FDisplayClusterLightCardEditorHelper(int32 RendererId)
: RendererId(RendererId), bCreatedRenderer(false)
{
}
FDisplayClusterLightCardEditorHelper::~FDisplayClusterLightCardEditorHelper()
{
if (bCreatedRenderer)
{
IDisplayClusterScenePreview::Get().DestroyRenderer(RendererId);
}
FWorldDelegates::OnWorldCleanup.RemoveAll(this);
#if WITH_EDITORONLY_DATA
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
if (ADisplayClusterRootActor* RootActor = CachedRootActor.Get())
{
if (UBlueprint* Blueprint = UBlueprint::GetBlueprintFromClass(RootActor->GetClass()))
{
Blueprint->OnCompiled().RemoveAll(this);
}
}
#endif
}
#if WITH_EDITOR
void FDisplayClusterLightCardEditorHelper::SetEditorViewportClient(TWeakPtr<FEditorViewportClient> InViewportClient)
{
ViewportClient = InViewportClient;
}
#endif
void FDisplayClusterLightCardEditorHelper::SetProjectionMode(EDisplayClusterMeshProjectionType Value)
{
ProjectionMode = Value;
}
EDisplayClusterMeshProjectionType FDisplayClusterLightCardEditorHelper::GetProjectionMode() const
{
return ProjectionMode;
}
void FDisplayClusterLightCardEditorHelper::SetIsOrthographic(bool bValue)
{
bIsOrthographic = bValue;
}
bool FDisplayClusterLightCardEditorHelper::GetIsOrthographic() const
{
return bIsOrthographic;
}
void FDisplayClusterLightCardEditorHelper::SetRootActor(ADisplayClusterRootActor& NewRootActor)
{
if (!ensureMsgf(bCreatedRenderer, TEXT("SetRootActor can't be called on an FDisplayClusterLightCardEditorHelper that was created with an existing preview renderer")))
{
return;
}
// Use custom settings for root actor.
FDisplayClusterRootActorPropertyOverrides PropertyOverrides;
{
PropertyOverrides.bPreviewICVFXFrustums = false;
PropertyOverrides.bEnablePreviewTechvis = false;
PropertyOverrides.bPreviewEnableOverlayMaterial = false;
PropertyOverrides.bFreezePreviewRender = false;
PropertyOverrides.bPreviewEnablePostProcess = true;
PropertyOverrides.bPreviewEnable = true;
// Can render a DCRA preview without the HoldoutComposite plugin.
PropertyOverrides.bPreviewEnableHoldoutComposite = false;
PropertyOverrides.PreviewSetttingsSource = EDisplayClusterConfigurationRootActorPreviewSettingsSource::RootActor;
}
IDisplayClusterScenePreview::Get().SetRendererRootActor(RendererId, &NewRootActor, PropertyOverrides);
}
void FDisplayClusterLightCardEditorHelper::SetLevelInstanceRootActor(ADisplayClusterRootActor& NewRootActor)
{
LevelInstanceRootActor = &NewRootActor;
}
const UTexture2D* FDisplayClusterLightCardEditorHelper::GetNormalMapTexture(bool bShowNorthMap)
{
// TODO: Exposure to the raw stage geometry normal maps was removed since they are generally undecipherable, and an upcoming task (UE-153862) will add a
// way to visualize the normal maps in a way that is far more user friendly
return nullptr;
}
void FDisplayClusterLightCardEditorHelper::MoveActorsToPixel(
const TArray<FDisplayClusterWeakStageActorPtr>& Actors, const FIntPoint& PixelPos, const FSceneView& SceneView)
{
UpdateProjectionOriginComponent();
FVector Origin;
FVector Direction;
CalculateOriginAndDirectionFromPixelPosition(PixelPos, SceneView, FVector::ZeroVector, Origin, Direction);
if (ProjectionMode == EDisplayClusterMeshProjectionType::UV)
{
// Find the average position of all selected actors. This group average is what is moved to the specified pixel
FVector2D AverageUVCoords = FVector2D::ZeroVector;
int32 NumLightCards = 0;
for (const FDisplayClusterWeakStageActorPtr& Actor : Actors)
{
if (Actor.IsValid() && Actor->IsUVActor())
{
AverageUVCoords += Actor->GetUVCoordinates();
++NumLightCards;
}
}
AverageUVCoords /= NumLightCards;
// Compute the desired coordinates by projecting the specified screen ray onto the UV projection plane
const float UVProjectionPlaneSize = ADisplayClusterLightCardActor::UVPlaneDefaultSize;
const float UVProjectionPlaneDistance = ADisplayClusterLightCardActor::UVPlaneDefaultDistance;
const FPlane UVProjectionPlane(FVector::ForwardVector * UVProjectionPlaneDistance, -FVector::ForwardVector);
const FVector PlaneIntersection = FMath::RayPlaneIntersection(FVector::ZeroVector, Direction, UVProjectionPlane);
const FVector2D DesiredUVCoords = FVector2D(PlaneIntersection.Y / UVProjectionPlaneSize + 0.5f, 0.5f - PlaneIntersection.Z / UVProjectionPlaneSize);
const FVector2D DeltaUVCoords = DesiredUVCoords - AverageUVCoords;
for (const FDisplayClusterWeakStageActorPtr& Actor : Actors)
{
if (Actor.IsValid() && Actor->IsUVActor())
{
Actor->SetUVCoordinates(Actor->GetUVCoordinates() + DeltaUVCoords);
}
}
}
else
{
// Find the average position of all selected light cards. This group average is what is moved to the specified pixel
FSphericalCoordinates AverageCoords = FSphericalCoordinates();
int32 NumActors = 0;
for (const FDisplayClusterWeakStageActorPtr& Actor : Actors)
{
if (Actor.IsValid())
{
VerifyAndFixActorOrigin(Actor);
const FSphericalCoordinates ActorCoords = GetActorCoordinates(Actor);
AverageCoords = AverageCoords + ActorCoords;
++NumActors;
}
}
if (NumActors == 0)
{
NumActors = 1;
}
AverageCoords.Radius /= NumActors;
AverageCoords.Azimuth /= NumActors;
AverageCoords.Inclination /= NumActors;
AverageCoords.Conform();
// Compute desired coordinates (radius doesn't matter here since we will use the flush constraint on the light cards after moving them)
const FVector LocalDirection = CachedRootActor->GetActorRotation().RotateVector(Direction);
const FSphericalCoordinates DesiredCoords(LocalDirection * 100.0f);
const FSphericalCoordinates DeltaCoords = DesiredCoords - AverageCoords;
// Update each light card with the delta coordinates; the flush constraint is applied by MoveLightCardTo, ensuring the light card is always flush to screens
for (const FDisplayClusterWeakStageActorPtr& LightCard : Actors)
{
if (LightCard.IsValid() && !LightCard->IsUVActor())
{
const FSphericalCoordinates LightCardCoords = GetActorCoordinates(LightCard);
const FSphericalCoordinates NewCoords = LightCardCoords + DeltaCoords;
MoveActorsTo({ LightCard }, NewCoords);
}
}
}
}
void FDisplayClusterLightCardEditorHelper::MoveActorsTo(
const TArray<FDisplayClusterWeakStageActorPtr>& Actors, const FSphericalCoordinates& SphericalCoords)
{
ADisplayClusterRootActor* RootActor = UpdateRootActor();
if (!RootActor)
{
return;
}
if (!UpdateNormalMaps())
{
return;
}
for (const FDisplayClusterWeakStageActorPtr& Actor : Actors)
{
if (!Actor.IsValid())
{
continue;
}
InternalMoveActorTo(Actor, SphericalCoords, true);
}
}
void FDisplayClusterLightCardEditorHelper::DragActors(
const TArray<FDisplayClusterWeakStageActorPtr>& Actors, const FIntPoint& PixelPos,
const FSceneView& SceneView, ECoordinateSystem CoordinateSystem, const FVector& DragWidgetOffset, EAxisList::Type DragAxis,
FDisplayClusterWeakStageActorPtr PrimaryActor)
{
if (Actors.IsEmpty() || !UpdateNormalMaps() || !UpdateRootActor())
{
return;
}
InternalDragActors(Actors, PixelPos, SceneView, CoordinateSystem, DragWidgetOffset, DragAxis, PrimaryActor);
}
void FDisplayClusterLightCardEditorHelper::DragUVActors(
const TArray<FDisplayClusterWeakStageActorPtr>& Actors, const FIntPoint& PixelPos, const FSceneView& SceneView,
const FVector& DragWidgetOffset, EAxisList::Type DragAxis, FDisplayClusterWeakStageActorPtr PrimaryActor)
{
if (Actors.IsEmpty() || !UpdateNormalMaps() || !UpdateRootActor())
{
return;
}
if (!PrimaryActor.IsValid())
{
PrimaryActor = Actors.Last();
}
if (!PrimaryActor.IsValid() || !PrimaryActor->IsUVActor())
{
return;
}
const FVector2D DeltaUV = GetUVActorTranslationDelta(PixelPos, SceneView, PrimaryActor, DragAxis, DragWidgetOffset);
for (const FDisplayClusterWeakStageActorPtr& Actor : Actors)
{
if (!Actor.IsValid() || !Actor->IsUVActor())
{
continue;
}
Actor->SetUVCoordinates(Actor->GetUVCoordinates() + DeltaUV);
#if WITH_EDITOR
if (!Actor->IsProxy())
{
PostEditChangePropertiesForMovedActor(Actor);
}
#endif
}
}
void FDisplayClusterLightCardEditorHelper::VerifyAndFixActorOrigin(const FDisplayClusterWeakStageActorPtr& Actor)
{
// Center actor on the current view point, let it keep its current world placement
// (but not its spin/yaw/pitch since that will be happen later using the cache)
const ADisplayClusterRootActor* RootActor = UpdateRootActor();
const ADisplayClusterRootActor* OwningRootActor = (Actor->IsProxy() || !LevelInstanceRootActor.IsValid()) ? RootActor : LevelInstanceRootActor.Get();
const USceneComponent* OriginComponent = nullptr;
if (OwningRootActor)
{
OriginComponent = Actor->IsProxy() && ProjectionOriginComponent.IsValid() ? ProjectionOriginComponent.Get() : OwningRootActor->GetCommonViewPoint();
}
if (!OriginComponent)
{
return;
}
// Set location and rotation to match the root actor
const FVector& NewActorLocation = OriginComponent ? OriginComponent->GetComponentLocation() : FVector::ZeroVector;
const FRotator& NewActorRotation = OwningRootActor ? OwningRootActor->GetActorRotation() : FRotator::ZeroRotator;
const FTransform OldOrigin = Actor->GetOrigin();
Actor->SetOrigin({ NewActorRotation, NewActorLocation, FVector::One() });
const FTransform NewOrigin = Actor->GetOrigin();
bool bActorMoved = !OldOrigin.Equals(NewOrigin, 0.f);
if (Actor.AsActorChecked()->IsA<ADisplayClusterLightCardActor>())
{
const FDisplayClusterPositionalParams OldPositionalParams = Actor->GetPositionalParams();
// Update the light card spherical coordinates to match its current world coordinates
const FVector LightCardEndEffectorLocation = Actor->GetStageActorTransform(true).GetLocation();
const FVector ActorRelativeLocation = NewActorRotation.UnrotateVector(LightCardEndEffectorLocation);
const FSphericalCoordinates SphericalCoords(ActorRelativeLocation);
SetActorCoordinates(Actor, SphericalCoords);
const FDisplayClusterPositionalParams NewPositionalParams = Actor->GetPositionalParams();
bActorMoved = bActorMoved || NewPositionalParams != OldPositionalParams;
}
#if WITH_EDITOR
if (bActorMoved)
{
PostEditChangePropertiesForMovedActor(Actor);
}
#endif
}
bool FDisplayClusterLightCardEditorHelper::CalculateNormalAndPositionInDirection(
const FVector& InOrigin,
const FVector& InDirection,
FVector& OutWorldPosition,
FVector& OutRelativeNormal,
double InDesiredDistanceFromFlush)
{
if (!UpdateNormalMaps())
{
return false;
}
if (ProjectionMode == EDisplayClusterMeshProjectionType::UV)
{
// In UV projection mode, all relevant geometry is projected onto the UV plane, so compute the world position and normal
// by performing a ray-plane intersection on the UV projection plane
const float UVProjectionPlaneDistance = ADisplayClusterLightCardActor::UVPlaneDefaultDistance;
const FPlane UVProjectionPlane(InOrigin + FVector::ForwardVector * UVProjectionPlaneDistance, -FVector::ForwardVector);
const FVector PlaneIntersection = FMath::RayPlaneIntersection(InOrigin, InDirection, UVProjectionPlane);
OutRelativeNormal = UVProjectionPlane.GetNormal();
OutWorldPosition = PlaneIntersection;
}
else
{
float Distance;
CachedRootActor->GetStageGeometryComponent()->GetStageDistanceAndNormal(InDirection, Distance, OutRelativeNormal);
Distance = CalculateFinalLightCardDistance(Distance, InDesiredDistanceFromFlush);
// Calculate world position
OutWorldPosition = InOrigin + Distance * InDirection;
}
return true;
}
bool FDisplayClusterLightCardEditorHelper::CalculateOriginAndDirectionFromPixelPosition(const FIntPoint& PixelPos, const FSceneView& SceneView, const FVector& OriginOffset,
FVector& OutOrigin, FVector& OutDirection)
{
PixelToWorld(SceneView, PixelPos, OutOrigin, OutDirection);
if (bIsOrthographic)
{
if (!UpdateNormalMaps())
{
return false;
}
// For orthogonal projections, PixelToWorld does not return the view point or a direction from the view point. Use TraceScreenRay
// to find a useful direction away from the origin to use
const FVector NewOrigin = SceneView.ViewLocation;
OutDirection = TraceScreenRay(OutOrigin + OriginOffset, OutDirection, NewOrigin);
OutOrigin = NewOrigin;
}
return true;
}
void FDisplayClusterLightCardEditorHelper::PixelToWorld(const FSceneView& View, const FIntPoint& PixelPos, FVector& OutOrigin, FVector& OutDirection) const
{
const FMatrix& InvProjMatrix = View.ViewMatrices.GetInvProjectionMatrix();
FMatrix InvViewMatrix = View.ViewMatrices.GetInvViewMatrix();
// Cancel out the root actor's orientation
if (USceneComponent* OriginComponent = ProjectionOriginComponent.Get())
{
if (const AActor* Actor = OriginComponent->GetOwner())
{
if (!bIsOrthographic)
{
FTransform Transform;
Transform.SetRotation(Actor->GetActorRotation().Quaternion());
Transform.SetTranslation(OriginComponent->GetComponentLocation());
InvViewMatrix *= Transform.ToInverseMatrixWithScale();
}
}
}
FVector4 ScreenPos = View.PixelToScreen(PixelPos.X, PixelPos.Y, 0);
ScreenPos.Z = 1; // Force near clip plane
FVector4 ViewPos = FVector(InvProjMatrix.TransformFVector4(ScreenPos));
ViewPos /= ViewPos.W;
if (bIsOrthographic)
{
ViewPos.Z = 0;
}
const FVector UnprojectedViewPos = FDisplayClusterMeshProjectionRenderer::UnprojectViewPosition(ViewPos, ProjectionMode);
if (bIsOrthographic)
{
OutOrigin = InvViewMatrix.TransformFVector4(UnprojectedViewPos);
OutDirection = InvViewMatrix.TransformVector(FVector(0, 0, 1)).GetSafeNormal();
}
else
{
OutOrigin = View.ViewMatrices.GetViewOrigin();
OutDirection = InvViewMatrix.TransformVector(UnprojectedViewPos).GetSafeNormal();
}
}
bool FDisplayClusterLightCardEditorHelper::WorldToPixel(const FSceneView& View, const FVector& WorldPos, FVector2D& OutPixelPos) const
{
return WorldToPixel(View, WorldPos, OutPixelPos, ProjectionMode);
}
bool FDisplayClusterLightCardEditorHelper::WorldToPixel(const FSceneView& View, const FVector& WorldPos, FVector2D& OutPixelPos, EDisplayClusterMeshProjectionType OverrideProjectionMode) const
{
const FMatrix& ViewMatrix = View.ViewMatrices.GetViewMatrix();
const FMatrix& ProjMatrix = View.ViewMatrices.GetProjectionMatrix();
const FVector ViewPos = ViewMatrix.TransformPosition(WorldPos);
const FVector ProjectedViewPos = FDisplayClusterMeshProjectionRenderer::ProjectViewPosition(ViewPos, OverrideProjectionMode);
const FVector4 ScreenPos = ProjMatrix.TransformFVector4(FVector4(ProjectedViewPos, 1));
return View.ScreenToPixel(ScreenPos, OutPixelPos);
}
void FDisplayClusterLightCardEditorHelper::GetSceneViewInitOptions(
FSceneViewInitOptions& OutViewInitOptions,
float InFOV,
const FIntPoint& InViewportSize,
const FVector& InLocation,
const FRotator& InRotation,
const EAspectRatioAxisConstraint InAspectRatioAxisConstraint,
float InNearClipPlane,
const FMatrix* InRotationMatrix,
float InDPIScale)
{
OutViewInitOptions.ViewLocation = InLocation;
OutViewInitOptions.ViewRotation = InRotation;
OutViewInitOptions.ViewOrigin = OutViewInitOptions.ViewLocation;
FIntPoint ViewportSize = InViewportSize;
ViewportSize.X = FMath::Max(ViewportSize.X, 1);
ViewportSize.Y = FMath::Max(ViewportSize.Y, 1);
FIntPoint ViewportOffset(0, 0);
OutViewInitOptions.SetViewRectangle(FIntRect(ViewportOffset, ViewportOffset + ViewportSize));
if (const ADisplayClusterRootActor* RootActor = UpdateRootActor())
{
const AWorldSettings* WorldSettings = nullptr;
if (RootActor->GetWorld() != nullptr)
{
WorldSettings = RootActor->GetWorld()->GetWorldSettings();
}
if (WorldSettings != nullptr)
{
OutViewInitOptions.WorldToMetersScale = WorldSettings->WorldToMeters;
}
}
// Rotate view 90 degrees
const FMatrix Rotate90(
FPlane(0, 0, 1, 0),
FPlane(1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, 0, 1)
);
OutViewInitOptions.ViewRotationMatrix = (InRotationMatrix ? *InRotationMatrix : FInverseRotationMatrix(OutViewInitOptions.ViewRotation)) * Rotate90;
// Avoid zero ViewFOV's which cause divide by zero's in projection matrix
const float RadianFOV = FMath::DegreesToRadians(InFOV);
const float HalfFOV = FMath::Max(0.001f, RadianFOV * 0.5f);
// Determine FOV multipliers to match render target's aspect ratio
float XAxisMultiplier;
float YAxisMultiplier;
if (((ViewportSize.X > ViewportSize.Y) && (InAspectRatioAxisConstraint == AspectRatio_MajorAxisFOV)) || (InAspectRatioAxisConstraint == AspectRatio_MaintainXFOV))
{
//if the viewport is wider than it is tall
XAxisMultiplier = 1.0f;
YAxisMultiplier = ViewportSize.X / (float)ViewportSize.Y;
}
else
{
//if the viewport is taller than it is wide
XAxisMultiplier = ViewportSize.Y / (float)ViewportSize.X;
YAxisMultiplier = 1.0f;
}
if (bIsOrthographic)
{
const float ZScale = 0.5f / UE_OLD_HALF_WORLD_MAX;
const float ZOffset = UE_OLD_HALF_WORLD_MAX;
const float FOVScale = FMath::Tan(HalfFOV) / InDPIScale;
const float OrthoWidth = 0.5f * FOVScale * ViewportSize.X;
const float OrthoHeight = 0.5f * FOVScale * ViewportSize.Y;
if ((bool)ERHIZBuffer::IsInverted)
{
OutViewInitOptions.ProjectionMatrix = FReversedZOrthoMatrix(
OrthoWidth,
OrthoHeight,
ZScale,
ZOffset
);
}
else
{
OutViewInitOptions.ProjectionMatrix = FOrthoMatrix(
OrthoWidth,
OrthoHeight,
ZScale,
ZOffset
);
}
}
else
{
if ((bool)ERHIZBuffer::IsInverted)
{
OutViewInitOptions.ProjectionMatrix = FReversedZPerspectiveMatrix(
HalfFOV,
HalfFOV,
XAxisMultiplier,
YAxisMultiplier,
InNearClipPlane,
InNearClipPlane
);
}
else
{
OutViewInitOptions.ProjectionMatrix = FPerspectiveMatrix(
HalfFOV,
HalfFOV,
XAxisMultiplier,
YAxisMultiplier,
InNearClipPlane,
InNearClipPlane
);
}
}
if (!OutViewInitOptions.IsValidViewRectangle())
{
// Zero sized rects are invalid, so fake to 1x1 to avoid asserts later on
OutViewInitOptions.SetViewRectangle(FIntRect(0, 0, 1, 1));
}
OutViewInitOptions.BackgroundColor = FLinearColor::Black;
OutViewInitOptions.FOV = InFOV;
}
void FDisplayClusterLightCardEditorHelper::ConfigureRenderProjectionSettings(FDisplayClusterMeshProjectionRenderSettings& OutRenderSettings, const FVector ViewLocation) const
{
OutRenderSettings.ProjectionType = ProjectionMode;
if (ProjectionMode == EDisplayClusterMeshProjectionType::UV)
{
OutRenderSettings.ProjectionTypeSettings.UVProjectionIndex = 1;
OutRenderSettings.ProjectionTypeSettings.UVProjectionPlaneSize = ADisplayClusterLightCardActor::UVPlaneDefaultSize;
OutRenderSettings.ProjectionTypeSettings.UVProjectionPlaneDistance = ADisplayClusterLightCardActor::UVPlaneDefaultDistance;
// Compute the UV plane offset to allow panning. Need to convert to view space, since the UV projection assumes all coordinates are in view space
const FMatrix WorldToViewTransform = FMatrix(FVector::ZAxisVector, FVector::XAxisVector, FVector::YAxisVector, FVector::ZeroVector);
OutRenderSettings.ProjectionTypeSettings.UVProjectionPlaneOffset = -WorldToViewTransform.TransformVector(ViewLocation);
}
}
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates FDisplayClusterLightCardEditorHelper::GetActorCoordinates(const FDisplayClusterWeakStageActorPtr& Actor)
{
const FVector ActorLocation = Actor->GetStageActorTransform(true).GetTranslation();
FSphericalCoordinates ActorSphericalCoords(ActorLocation);
// If the light card points at any of the poles, the spherical coordinates will have an "undefined" azimuth value.
// For continuity when dragging a light card positioned there,
// we can manually set the azimuthal value to match the light card's configured longitude
if (ActorSphericalCoords.IsPointingAtPole())
{
ActorSphericalCoords.Azimuth = FMath::DegreesToRadians(Actor->GetLongitude() + 180.f);
}
return ActorSphericalCoords;
}
FDisplayClusterMeshProjectionPrimitiveFilter::FPrimitiveFilter FDisplayClusterLightCardEditorHelper::CreateDefaultShouldRenderPrimitiveFilter() const
{
const bool bIsUVProjection = ProjectionMode == EDisplayClusterMeshProjectionType::UV;
// Create a lambda function so that it's safe to access even if this helper is destroyed
return FDisplayClusterMeshProjectionPrimitiveFilter::FPrimitiveFilter::CreateLambda([bIsUVProjection](const UPrimitiveComponent* PrimitiveComponent)
{
if (IDisplayClusterStageActor* StageActor = Cast<IDisplayClusterStageActor>(PrimitiveComponent->GetOwner()))
{
// Only render the UV actors when in UV projection mode, and only render non-UV actors in any other projection mode
return bIsUVProjection ? StageActor->IsUVActor() : !StageActor->IsUVActor();
}
return true;
});
}
FDisplayClusterMeshProjectionPrimitiveFilter::FPrimitiveFilter FDisplayClusterLightCardEditorHelper::CreateDefaultShouldApplyProjectionToPrimitiveFilter() const
{
const bool bIsUVProjection = ProjectionMode == EDisplayClusterMeshProjectionType::UV;
// Create a lambda function so that it's safe to access even if this helper is destroyed
return FDisplayClusterMeshProjectionPrimitiveFilter::FPrimitiveFilter::CreateLambda([bIsUVProjection](const UPrimitiveComponent* PrimitiveComponent)
{
if (IDisplayClusterStageActor* StageActor = Cast<IDisplayClusterStageActor>(PrimitiveComponent->GetOwner()))
{
// When in UV projection mode, don't render the UV actors using the UV projection, render them linearly
if (bIsUVProjection && StageActor->IsUVActor())
{
return false;
}
}
return true;
});
}
AActor* FDisplayClusterLightCardEditorHelper::SpawnStageActor(const FSpawnActorArgs& InSpawnArgs)
{
ADisplayClusterRootActor* RootActor = InSpawnArgs.RootActor;
const TSubclassOf<AActor> ActorClass = InSpawnArgs.ActorClass;
const UDisplayClusterStageActorTemplate* Template = InSpawnArgs.Template;
check(RootActor)
check(ActorClass || Template);
FName ActorName = InSpawnArgs.ActorName;
const EDisplayClusterMeshProjectionType ProjectionMode = InSpawnArgs.ProjectionMode;
ULevel* Level = InSpawnArgs.Level ? InSpawnArgs.Level : RootActor->GetWorld()->GetCurrentLevel();
const bool bIsPreview = InSpawnArgs.bIsPreview;
AActor* NewActor = nullptr;
if (Template)
{
const AActor* TemplateActor = Template->GetTemplateActor();
check(TemplateActor);
// For now only light card templates are supported
check(TemplateActor->GetClass()->IsChildOf(ADisplayClusterLightCardActor::StaticClass()));
FName UniqueName = *Template->GetName().Replace(TEXT("Template"), TEXT(""));
if (StaticFindObjectFast(TemplateActor->GetClass(), Level, UniqueName))
{
UniqueName = MakeUniqueObjectName(Level, TemplateActor->GetClass(), UniqueName);
}
// Duplicate, don't copy properties or spawn from a template. Doing so will copy component data incorrectly,
// specifically the static mesh override textures. They will be parented to the template, not the level instance
// and prevent the map from saving.
ADisplayClusterLightCardActor* NewLightCard = CastChecked<ADisplayClusterLightCardActor>(StaticDuplicateObject(TemplateActor, Level, UniqueName));
ActorName = UniqueName;
#if WITH_EDITOR
Level->AddLoadedActor(NewLightCard);
#endif
NewActor = NewLightCard;
}
else
{
const FVector SpawnLocation = RootActor->GetDefaultCamera()->GetComponentLocation();
FRotator SpawnRotation = RootActor->GetDefaultCamera()->GetComponentRotation();
SpawnRotation.Yaw -= 180.f;
FActorSpawnParameters SpawnParameters;
SpawnParameters.bNoFail = true;
SpawnParameters.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
SpawnParameters.Name = ActorName;
SpawnParameters.NameMode = FActorSpawnParameters::ESpawnActorNameMode::Requested;
SpawnParameters.OverrideLevel = Level;
NewActor = CastChecked<AActor>(
RootActor->GetWorld()->SpawnActor(ActorClass,
&SpawnLocation, &SpawnRotation, MoveTemp(SpawnParameters)));
}
check(NewActor);
#if WITH_EDITOR
if (!bIsPreview)
{
FActorLabelUtilities::SetActorLabelUnique(NewActor, ActorName.ToString());
}
#endif
if (ADisplayClusterLightCardActor* NewLightCard = Cast<ADisplayClusterLightCardActor>(NewActor))
{
if (ProjectionMode == EDisplayClusterMeshProjectionType::UV)
{
// If this already is a UV light card leave everything alone
if (!NewLightCard->bIsUVLightCard)
{
NewLightCard->bIsUVLightCard = true;
NewLightCard->Scale /= 4;
// Don't override feathering if this was spawned from a template
if (!Template)
{
NewLightCard->Feathering = 0.05; // Just enough to avoid jagged look on UV lightcards.
}
}
}
FDisplayClusterLabelConfiguration LabelConfiguration = InSpawnArgs.AddLightCardArgs.LabelConfiguration;
LabelConfiguration.RootActor = RootActor;
NewLightCard->ShowLightCardLabel(LabelConfiguration);
if (ADisplayClusterChromakeyCardActor* ChromakeyCardActor = Cast<ADisplayClusterChromakeyCardActor>(NewActor))
{
ChromakeyCardActor->AddToRootActor(RootActor);
}
else if (!bIsPreview)
{
AddLightCardsToRootActor({ NewLightCard }, RootActor, InSpawnArgs.AddLightCardArgs);
}
}
#if WITH_EDITOR
// Need to call this if spawned from a template since this would normally be called in SpawnActor
if (Template && GIsEditor)
{
GEditor->BroadcastLevelActorAdded(NewActor);
}
#endif
return NewActor;
}
void FDisplayClusterLightCardEditorHelper::AddLightCardsToRootActor(
const TArray<ADisplayClusterLightCardActor*>& LightCards, ADisplayClusterRootActor* RootActor,
const FAddLightCardArgs& AddLightCardArgs)
{
if (LightCards.Num() == 0)
{
return;
}
check(RootActor);
UDisplayClusterConfigurationData* ConfigData = RootActor->GetConfigData();
ConfigData->Modify();
FDisplayClusterConfigurationICVFX_VisibilityList& RootActorLightCards = ConfigData->StageSettings.Lightcard.ShowOnlyList;
for (ADisplayClusterLightCardActor* LightCard : LightCards)
{
check(LightCard);
if (!RootActorLightCards.Actors.ContainsByPredicate([&](const TSoftObjectPtr<AActor>& Actor)
{
// Don't add if a loaded actor is already present.
return Actor.Get() == LightCard;
}))
{
const TSoftObjectPtr<AActor> LightCardSoftObject(LightCard);
// Remove any exact paths to this actor. It's possible invalid actors are present if a light card
// was force deleted from a level.
RootActorLightCards.Actors.RemoveAll([&](const TSoftObjectPtr<AActor>& Actor)
{
return Actor == LightCardSoftObject;
});
LightCard->AddToRootActor(RootActor);
}
#if WITH_EDITOR
// Fire this event so that e.g. ICVFX panel can update proxies based on the new root/layer
LightCard->PostEditChange();
#endif
}
}
ADisplayClusterRootActor* FDisplayClusterLightCardEditorHelper::UpdateRootActor()
{
ADisplayClusterRootActor* NewRootActor = IDisplayClusterScenePreview::Get().GetRendererRootActor(RendererId);
if (NewRootActor == CachedRootActor)
{
return NewRootActor;
}
#if WITH_EDITOR
// Clean up old subscribed events
if (ADisplayClusterRootActor* RootActor = CachedRootActor.Get())
{
if (UBlueprint* Blueprint = UBlueprint::GetBlueprintFromClass(RootActor->GetClass()))
{
Blueprint->OnCompiled().RemoveAll(this);
}
}
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
#endif
CachedRootActor = NewRootActor;
InvalidateNormalMap();
if (NewRootActor)
{
RootActorBoundingRadius = NewRootActor->GetStageGeometryComponent()->GetStageBoundingRadius();
#if WITH_EDITOR
FCoreUObjectDelegates::OnObjectPropertyChanged.AddRaw(this, &FDisplayClusterLightCardEditorHelper::OnActorPropertyChanged);
if (UBlueprint* Blueprint = UBlueprint::GetBlueprintFromClass(NewRootActor->GetClass()))
{
Blueprint->OnCompiled().AddRaw(this, &FDisplayClusterLightCardEditorHelper::OnRootActorBlueprintCompiled);
}
#endif
}
else
{
RootActorBoundingRadius = 0.f;
}
return NewRootActor;
}
USceneComponent* FDisplayClusterLightCardEditorHelper::UpdateProjectionOriginComponent()
{
if (ADisplayClusterRootActor* RootActor = UpdateRootActor())
{
ProjectionOriginComponent = RootActor->GetCommonViewPoint();
}
else
{
ProjectionOriginComponent = nullptr;
}
return ProjectionOriginComponent.Get();
}
FVector FDisplayClusterLightCardEditorHelper::GetProjectionOrigin() const
{
if (ProjectionOriginComponent.IsValid())
{
return ProjectionOriginComponent->GetComponentTransform().GetLocation();
}
if (CachedRootActor.IsValid())
{
return CachedRootActor->GetTransform().GetLocation();
}
return FVector::Zero();
}
FRotator FDisplayClusterLightCardEditorHelper::GetActorRotationDelta(const FIntPoint& PixelPos, const FSceneView& View, const FDisplayClusterWeakStageActorPtr& Actor,
ECoordinateSystem CoordinateSystem, EAxisList::Type DragAxis, const FVector& DragWidgetOffset)
{
const FSphericalCoordinates DeltaCoords = GetActorTranslationDelta(PixelPos, View, Actor, CoordinateSystem, DragAxis, DragWidgetOffset);
const FSphericalCoordinates LightCardCoords = GetActorCoordinates(Actor);
const FSphericalCoordinates NewCoords = LightCardCoords + DeltaCoords;
const FVector LightCardPos = LightCardCoords.AsCartesian();
const FVector NewPos = NewCoords.AsCartesian();
const FVector PosCrossProduct = FVector::CrossProduct(LightCardPos, NewPos);
if (FMath::IsNearlyZero(PosCrossProduct.Length()))
{
return FQuat(FVector::ForwardVector, 0).Rotator();
}
const FVector AxisOfRotation = PosCrossProduct.GetSafeNormal();
const double Angle = FMath::Acos(FVector::DotProduct(LightCardPos.GetSafeNormal(), NewPos.GetSafeNormal()));
return FQuat(AxisOfRotation, Angle).Rotator();
}
FDisplayClusterLightCardEditorHelper::FSphericalCoordinates FDisplayClusterLightCardEditorHelper::GetActorTranslationDelta(
const FIntPoint& PixelPos,
const FSceneView& View,
const FDisplayClusterWeakStageActorPtr& Actor,
ECoordinateSystem CoordinateSystem,
EAxisList::Type DragAxis,
const FVector& DragWidgetOffset)
{
FVector Origin;
FVector Direction;
CalculateOriginAndDirectionFromPixelPosition(PixelPos, View, -DragWidgetOffset, Origin, Direction);
if (!bIsOrthographic)
{
Direction = (Direction - DragWidgetOffset).GetSafeNormal();
}
checkSlow(CachedRootActor.IsValid());
const FVector LocalDirection = CachedRootActor->GetActorRotation().RotateVector(Direction);
const FVector LightCardLocation = Actor->GetStageActorTransform().GetTranslation() - Origin;
const FSphericalCoordinates ActorCoords = GetActorCoordinates(Actor);
FSphericalCoordinates DeltaCoords;
// If we are in a cartesian coordinate system and are constraining to an axis, perform the constraint calculations in
// cartesian coordinates, then convert to spherical coordinates at the end. Otherwise, perform all calculations in spherical coordinates
if (CoordinateSystem == ECoordinateSystem::Cartesian && DragAxis != EAxisList::Type::XYZ)
{
// For consistency, project the cursor direction vector onto the sphere the light card is currently on. Gives a good balance of approximating
// the true projection plane (one that works with all view projections) and the general stage normal map
const FVector RequestedLocation = LocalDirection * LightCardLocation.Size();
// Compute the axis to constrain the translation to based on the axis that was dragged. Axis must be rotated into the light card's local space
FVector Axis = FVector::ZeroVector;
if (DragAxis == EAxisList::Type::X)
{
Axis = FVector::XAxisVector;
}
else if (DragAxis == EAxisList::Type::Y)
{
Axis = FVector::YAxisVector;
}
else if (DragAxis == EAxisList::Type::Z)
{
Axis = FVector::ZAxisVector;
}
const FVector LocalAxis = CachedRootActor->GetActorRotation().RotateVector(Axis);
// Compute the offset between the requested location and the light card's current location, and project that
// offset onto the constraint axis
const FVector DeltaLocation = ((RequestedLocation - LightCardLocation) | LocalAxis) * LocalAxis;
const FVector ConstrainedLocation = LightCardLocation + DeltaLocation;
const FSphericalCoordinates ConstrainedCoords(ConstrainedLocation);
DeltaCoords = ConstrainedCoords - ActorCoords;
}
else
{
FVector FlushPosition;
FVector FlushNormal;
CachedRootActor->GetFlushPositionAndNormal(Actor->GetStageActorTransform().GetTranslation(), FlushPosition, FlushNormal);
const float Distance = (FlushPosition - CachedRootActor->GetCommonViewPoint()->GetComponentLocation()).Size();
const FSphericalCoordinates RequestedCoords(LocalDirection * Distance);
DeltaCoords = RequestedCoords - ActorCoords;
if (CoordinateSystem == ECoordinateSystem::Spherical)
{
if (DragAxis == EAxisList::Type::X)
{
DeltaCoords.Inclination = 0;
}
else if (DragAxis == EAxisList::Type::Y)
{
// Convert the inclination to Cartesian coordinates, project it to the x-z plane, and convert back to spherical coordinates. This ensures that the motion in the inclination
// plane always lines up with the mouse's projected location along that plane
const double FixedInclination = FMath::Abs(FMath::Atan2(
FMath::Cos(DeltaCoords.Azimuth) * FMath::Sin(RequestedCoords.Inclination),
FMath::Cos(RequestedCoords.Inclination))
);
// When translating along the inclination axis, the azimuth delta can only be intervals of pi
const double FixedAzimuth = FMath::RoundToInt(DeltaCoords.Azimuth / PI) * PI;
DeltaCoords.Azimuth = FixedAzimuth;
DeltaCoords.Inclination = FixedInclination - ActorCoords.Inclination;
}
}
}
return DeltaCoords;
}
FVector2D FDisplayClusterLightCardEditorHelper::GetUVActorTranslationDelta(
const FIntPoint& PixelPos,
const FSceneView& View,
const FDisplayClusterWeakStageActorPtr& Actor,
EAxisList::Type DragAxis,
const FVector& DragWidgetOffset)
{
FVector Origin;
FVector Direction;
PixelToWorld(View, PixelPos, Origin, Direction);
const float UVProjectionPlaneSize = ADisplayClusterLightCardActor::UVPlaneDefaultSize;
const float UVProjectionPlaneDistance = ADisplayClusterLightCardActor::UVPlaneDefaultDistance;
const FVector ViewOrigin = View.ViewMatrices.GetViewOrigin();
const FPlane UVProjectionPlane(ViewOrigin + FVector::ForwardVector * UVProjectionPlaneDistance, -FVector::ForwardVector);
const FVector PlaneIntersection = FMath::RayPlaneIntersection(Origin, Direction, UVProjectionPlane);
const FVector DesiredLocation = PlaneIntersection - DragWidgetOffset;
const FVector2D DesiredUVLocation = FVector2D(DesiredLocation.Y / UVProjectionPlaneSize + 0.5f, 0.5f - DesiredLocation.Z / UVProjectionPlaneSize);
const FVector2D UVDelta = DesiredUVLocation - Actor->GetUVCoordinates();
FVector2D UVAxis = FVector2D::ZeroVector;
if (DragAxis & EAxisList::Type::X)
{
UVAxis += FVector2D(1.0, 0.0);
}
if (DragAxis & EAxisList::Type::Y)
{
UVAxis += FVector2D(0.0, 1.0);
}
return UVDelta * UVAxis;
}
void FDisplayClusterLightCardEditorHelper::InternalMoveActorTo(
const FDisplayClusterWeakStageActorPtr& Actor, const FSphericalCoordinates& Position, bool bIsFinalChange) const
{
if (bNormalMapInvalid || !CachedRootActor.IsValid())
{
ensure(false);
return;
}
// Remove actor rotation from the position before setting the coordinate. Note that we don't need to do this for the normal map,
// which already takes the root actor rotation into account as part of its view matrix.
const FQuat RootRotation = CachedRootActor->GetTransform().GetRotation().Inverse();
const FVector InverseRotatedPosition = RootRotation.RotateVector(Position.AsCartesian());
SetActorCoordinates(Actor, FSphericalCoordinates(InverseRotatedPosition));
Actor->UpdateStageActorTransform();
if (Actor->IsAlwaysFlushToWall())
{
ADisplayClusterRootActor* RootActor = (Actor->IsProxy() || !LevelInstanceRootActor.IsValid()) ? CachedRootActor.Get() : LevelInstanceRootActor.Get();
RootActor->MakeStageActorFlushToWall(Actor.AsActor());
}
#if WITH_EDITOR
if (!Actor->IsProxy())
{
if (bIsFinalChange)
{
Actor->UpdateEditorGizmos();
PostEditChangePropertiesForMovedActor(Actor);
}
}
#endif
}
void FDisplayClusterLightCardEditorHelper::InternalDragActors(const TArray<FDisplayClusterWeakStageActorPtr>& Actors, const FIntPoint& PixelPos, const FSceneView& View,
ECoordinateSystem CoordinateSystem, const FVector& DragWidgetOffset, EAxisList::Type DragAxis, FDisplayClusterWeakStageActorPtr PrimaryActor)
{
if (Actors.IsEmpty() || !ensure(!bNormalMapInvalid) || !UpdateRootActor())
{
return;
}
if (!PrimaryActor.IsValid())
{
PrimaryActor = Actors.Last();
}
if (PrimaryActor.IsValid() && !PrimaryActor->IsUVActor())
{
const bool bUseDeltaRotation = (DragAxis == EAxisList::Type::XYZ) || (DragAxis == EAxisList::Type::Y) || (CoordinateSystem == ECoordinateSystem::Cartesian);
const FRotator DeltaRotation =
bUseDeltaRotation ?
GetActorRotationDelta(PixelPos, View, PrimaryActor, CoordinateSystem, DragAxis, DragWidgetOffset)
: FRotator::ZeroRotator;
const FSphericalCoordinates DeltaCoords =
bUseDeltaRotation ?
FSphericalCoordinates()
: GetActorTranslationDelta(PixelPos, View, PrimaryActor, CoordinateSystem, DragAxis, DragWidgetOffset);
for (const FDisplayClusterWeakStageActorPtr& Actor : Actors)
{
if (!Actor.IsValid() || Actor->IsUVActor())
{
continue;
}
VerifyAndFixActorOrigin(Actor);
// Note: GetActorCoordinates maintains last known Azimuth when looking at the poles
const FSphericalCoordinates CurrentCoords = GetActorCoordinates(Actor);
// We will adjust the spin (to maintain the apparent spin) when using center of gizmo
// or dragging longitudinally (this seems to provide an intuitive behavior)
if (bUseDeltaRotation) // Dragging center of gizmo
{
// We might need this to put back the LightCard exactly as it was
const FDisplayClusterPositionalParams OriginalPositionalParams = Actor->GetPositionalParams();
const FVector CurrentPos = CurrentCoords.AsCartesian();
const FVector NewPos = DeltaRotation.RotateVector(CurrentPos);
// Calculations are only valid if translation is not too small
const FSphericalCoordinates NewCoords(NewPos);
const FTransform Transform_A = Actor->GetStageActorTransform(true);
// Don't fire off property change events yet since we're not done modifying the lightcard
InternalMoveActorTo(Actor, NewCoords, false);
Actor->UpdateStageActorTransform(); // We must call this for GetStageActorTransform to be valid
const FTransform Transform_B = Actor->GetStageActorTransform(true);
// Calculate world delta translation of moving from A to B
FVector WorldDelta = Transform_B.GetLocation() - Transform_A.GetLocation(); // X towards front of stage. Y towards right of stage. Z towards ceiling.
// Calculate LC "Y" unit vector at A and B. ("X" is LC normal)
const FVector Y_A = Transform_A.Rotator().RotateVector(FVector::YAxisVector);
const FVector Y_B = Transform_B.Rotator().RotateVector(FVector::YAxisVector);
// Calculate card normal vector
const FVector CardNormal_A = Transform_A.Rotator().RotateVector(FVector::XAxisVector); // When card is on ceiling, expect around (0,0,-1).
const FVector CardNormal_B = Transform_B.Rotator().RotateVector(FVector::XAxisVector);
// Calculate projection of movement onto surface tangent plane at A and B
const FVector UnitWorldDeltaPlane_A = FVector::VectorPlaneProject(WorldDelta, CardNormal_A).GetSafeNormal();
const FVector UnitWorldDeltaPlane_B = FVector::VectorPlaneProject(WorldDelta, CardNormal_B).GetSafeNormal();
if (!FMath::IsNearlyZero(UnitWorldDeltaPlane_A.Length()) && !FMath::IsNearlyZero(UnitWorldDeltaPlane_B.Length()))
{
// Calculate relative spin angle at A, which is the angle between Y_A and UnitWorldDeltaPlane_A
const double SpinDotProduct_A = FVector::DotProduct(Y_A, UnitWorldDeltaPlane_A);
const FVector SpinCrossProduct_A = FVector::CrossProduct(Y_A, UnitWorldDeltaPlane_A);
const int32 SpinSign_A = FVector::DotProduct(CardNormal_A, SpinCrossProduct_A) > 0 ? -1 : 1;
const double RelativeSpinAngle_A = FMath::Acos(SpinDotProduct_A) * SpinSign_A; // radians
// Now we need to find the spin that keeps the same RelativeSpinAngle in B as it was in A
const double SpinDotProduct_B = FVector::DotProduct(Y_B, UnitWorldDeltaPlane_B);
const FVector SpinCrossProduct_B = FVector::CrossProduct(Y_B, UnitWorldDeltaPlane_B);
const int32 SpinSign_B = FVector::DotProduct(CardNormal_B, SpinCrossProduct_B) > 0 ? -1 : 1;
const double RelativeSpinAngle_B = FMath::Acos(SpinDotProduct_B) * SpinSign_B; // radians
const double DeltaSpin = RelativeSpinAngle_B - RelativeSpinAngle_A;
// Apply delta spin to lightcard
Actor->SetSpin(Actor->GetSpin() + FMath::RadiansToDegrees(DeltaSpin));
}
else
{
// Leave it where it was to avoid apparent spins even though motion would have been insignificant.
Actor->SetPositionalParams(OriginalPositionalParams);
}
#if WITH_EDITOR
PostEditChangePropertiesForMovedActor(Actor);
#endif
}
else // Dragging latitudinally
{
InternalMoveActorTo(Actor, CurrentCoords + DeltaCoords, true);
}
}
}
}
void FDisplayClusterLightCardEditorHelper::SetActorCoordinates(const FDisplayClusterWeakStageActorPtr& Actor, const FSphericalCoordinates& SphericalCoords) const
{
Actor->SetDistanceFromCenter(SphericalCoords.Radius);
Actor->SetLatitude(90.f - FMath::RadiansToDegrees(SphericalCoords.Inclination));
// Keep the same longitude when pointing at the pole. This helps with continuity
// and also mitigates sudden changes in apparent spin when moving around the poles
if (!SphericalCoords.IsPointingAtPole())
{
Actor->SetLongitude(FRotator::ClampAxis(FMath::RadiansToDegrees(SphericalCoords.Azimuth) - 180.f));
}
}
bool FDisplayClusterLightCardEditorHelper::TraceStage(const FVector& RayStart, const FVector& RayEnd, FVector& OutHitLocation)
{
ADisplayClusterRootActor* RootActor = UpdateRootActor();
if (RootActor)
{
FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(DisplayClusterStageTrace), true);
FHitResult HitResult;
if (RootActor->ActorLineTraceSingle(HitResult, RayStart, RayEnd, ECollisionChannel::ECC_PhysicsBody, TraceParams))
{
OutHitLocation = HitResult.Location;
return true;
}
}
OutHitLocation = FVector::ZeroVector;
return false;
}
FVector FDisplayClusterLightCardEditorHelper::TraceScreenRay(const FVector& OrthogonalOrigin, const FVector& OrthogonalDirection, const FVector& ViewOrigin)
{
const FVector RayStart = OrthogonalOrigin;
const FVector RayEnd = OrthogonalOrigin + OrthogonalDirection * WORLD_MAX;
FVector Direction = FVector::ZeroVector;
if (ProjectionMode == EDisplayClusterMeshProjectionType::UV)
{
// In the UV projection mode, all stage geometry has been projected onto a UV projection plane, so perform a ray trace against that plane to find the
// screen ray direction
const float UVProjectionPlaneDistance = ADisplayClusterLightCardActor::UVPlaneDefaultDistance;
const FPlane UVProjectionPlane(ViewOrigin + FVector::ForwardVector * UVProjectionPlaneDistance, -FVector::ForwardVector);
const FVector PlaneIntersection = FMath::RayPlaneIntersection(OrthogonalOrigin, OrthogonalDirection, UVProjectionPlane);
Direction = (PlaneIntersection - ViewOrigin).GetSafeNormal();
}
else
{
// First, trace against the stage actor to see if the screen ray hits it; if so, simply return the direction from the view point to this hit point
FVector HitLocation = FVector::ZeroVector;
if (TraceStage(RayStart, RayEnd, HitLocation))
{
Direction = (HitLocation - ViewOrigin).GetSafeNormal();
}
else
{
// If we didn't hit any stage geometry, try to trace against the normal map mesh.
FCollisionQueryParams TraceParams(SCENE_QUERY_STAT(DisplayClusterStageTrace), true);
FHitResult HitResult;
if (ensure(!bNormalMapInvalid) && NormalMapMeshComponent->LineTraceComponent(HitResult, RayStart, RayEnd, TraceParams))
{
HitLocation = HitResult.Location;
Direction = (HitLocation - ViewOrigin).GetSafeNormal();
}
else
{
// If the screen ray does not hit the stage or the normal map mesh, then simply use the closest point on the ray to the view point
const FVector ClosestPoint = OrthogonalOrigin + ((ViewOrigin - OrthogonalOrigin) | OrthogonalDirection) * OrthogonalDirection;
Direction = (ClosestPoint - ViewOrigin).GetSafeNormal();
}
}
ADisplayClusterRootActor* RootActor = UpdateRootActor();
if (RootActor)
{
// Rotate direction back into local space
const FQuat RootRotation = RootActor->GetTransform().GetRotation().Inverse();
Direction = RootRotation.RotateVector(Direction);
}
}
return Direction;
}
double FDisplayClusterLightCardEditorHelper::CalculateFinalLightCardDistance(double FlushDistance, double DesiredOffsetFromFlush) const
{
double Distance = FMath::Min(FlushDistance, RootActorBoundingRadius) + DesiredOffsetFromFlush;
return FMath::Max(Distance, 0);
}
void FDisplayClusterLightCardEditorHelper::InvalidateNormalMap()
{
bNormalMapInvalid = true;
}
bool FDisplayClusterLightCardEditorHelper::UpdateNormalMaps()
{
if (!bNormalMapInvalid && NormalMapMeshComponent.IsValid())
{
return true;
}
ADisplayClusterRootActor* RootActor = UpdateRootActor();
if (!RootActor)
{
return false;
}
// Update this so we render from the latest projection point
UpdateProjectionOriginComponent();
RootActor->GetStageGeometryComponent()->Invalidate(true);
bNormalMapInvalid = false;
UpdateNormalMapMesh();
return !bNormalMapInvalid;
}
void FDisplayClusterLightCardEditorHelper::UpdateNormalMapMesh()
{
if (!NormalMeshScene.IsValid() || !IsValid(NormalMeshScene->GetWorld()))
{
FWorldDelegates::OnWorldCleanup.RemoveAll(this);
FWorldDelegates::OnWorldCleanup.AddRaw(this, &FDisplayClusterLightCardEditorHelper::OnWorldCleanup);
NormalMeshScene = MakeShared<FPreviewScene>(FPreviewScene::ConstructionValues());
}
else if (NormalMapMeshComponent.IsValid())
{
NormalMeshScene->RemoveComponent(NormalMapMeshComponent.Get());
}
UpdateProjectionOriginComponent();
const FName UniqueName = MakeUniqueObjectName(GetTransientPackage(), UProceduralMeshComponent::StaticClass(), TEXT("NormalMapMesh"));
NormalMapMeshComponent = NewObject<UProceduralMeshComponent>(GetTransientPackage(), UniqueName);
NormalMapMeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
NormalMeshScene->AddComponent(NormalMapMeshComponent.Get(), FTransform(GetProjectionOrigin()));
UStaticMesh* IcoSphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/nDisplay/Meshes/SM_IcoSphere.SM_IcoSphere"), nullptr, LOAD_None, nullptr);
if (ensure(IcoSphereMesh))
{
IcoSphereMesh->bAllowCPUAccess = true;
int32 NumSections = IcoSphereMesh->GetNumSections(0);
for (int32 SectionIndex = 0; SectionIndex < NumSections; SectionIndex++)
{
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UVs;
TArray<FProcMeshTangent> Tangents;
UKismetProceduralMeshLibrary::GetSectionFromStaticMesh(IcoSphereMesh, 0, SectionIndex, Vertices, Triangles, Normals, UVs, Tangents);
TArray<FVector2D> EmptyUVs;
TArray<FLinearColor> EmptyColors;
NormalMapMeshComponent.Get()->CreateMeshSection_LinearColor(SectionIndex, Vertices, Triangles, Normals, UVs, EmptyUVs, EmptyUVs, EmptyUVs, EmptyColors, Tangents, true);
for (int32 Index = 0; Index < IcoSphereMesh->GetStaticMaterials().Num(); ++Index)
{
UMaterialInterface* MaterialInterface = IcoSphereMesh->GetStaticMaterials()[Index].MaterialInterface;
NormalMapMeshComponent.Get()->SetMaterial(Index, MaterialInterface);
}
}
CachedRootActor->GetStageGeometryComponent()->MorphProceduralMesh(NormalMapMeshComponent.Get());
}
}
void FDisplayClusterLightCardEditorHelper::OnWorldCleanup(UWorld* World, bool bSessionEnded, bool bCleanupResources)
{
if (!NormalMeshScene.IsValid() || !CachedRootActor.IsValid() || World != CachedRootActor->GetWorld())
{
return;
}
if (NormalMapMeshComponent.IsValid())
{
NormalMeshScene->RemoveComponent(NormalMapMeshComponent.Get());
}
NormalMeshScene.Reset();
}
#if WITH_EDITORONLY_DATA
void FDisplayClusterLightCardEditorHelper::OnActorPropertyChanged(UObject* ObjectBeingModified, FPropertyChangedEvent& PropertyChangedEvent)
{
if (!CachedRootActor.IsValid())
{
FCoreUObjectDelegates::OnObjectPropertyChanged.RemoveAll(this);
return;
}
if (ObjectBeingModified == CachedRootActor.Get())
{
InvalidateNormalMap();
}
}
void FDisplayClusterLightCardEditorHelper::OnRootActorBlueprintCompiled(UBlueprint* Blueprint)
{
InvalidateNormalMap();
}
#endif
#if WITH_EDITOR
void FDisplayClusterLightCardEditorHelper::PostEditChangePropertiesForMovedActor(const FDisplayClusterWeakStageActorPtr& Actor) const
{
if (Actor->IsProxy())
{
return;
}
Actor.AsActorChecked()->Modify();
auto ModifyLightCardProperty = [&](const IDisplayClusterStageActor::FPropertyPair& PropertyPair) -> void
{
// Broadcast the event directly instead of PostEditChangeProperty on the object itself, which would attempt to create unnecessary snapshots for each call
FPropertyChangedEvent PropertyChangedEvent(PropertyPair.Value, EPropertyChangeType::Interactive);
FCoreUObjectDelegates::OnObjectPropertyChanged.Broadcast(Actor.AsActorChecked(), PropertyChangedEvent);
};
IDisplayClusterStageActor::FPositionalPropertyArray PropertyPairs;
Actor->GetPositionalProperties(PropertyPairs);
TArray<const FProperty*> ChangedProperties;
ChangedProperties.Reserve(PropertyPairs.Num());
for (const IDisplayClusterStageActor::FPropertyPair& PropertyPair : PropertyPairs)
{
ModifyLightCardProperty(PropertyPair);
ChangedProperties.Add(PropertyPair.Value);
}
SnapshotTransactionBuffer(Actor.AsActorChecked(), MakeArrayView(ChangedProperties.GetData(), ChangedProperties.Num()));
// Force the actor to update its position in a way that the multi-user server will see
Actor.AsActorChecked()->PostEditMove(false);
}
#endif