Project World to Screen Location stops returning a value

Hi,

I’m currently working on an blueprint implementation of https://forums.unrealengine.com/showthread.php?59398-Easy-Offscreen-Indicator-Blueprint-Node

I’ve managed to create a working script, however I’m trying to get my head around the ‘Project World To Screen’ node. It takes in a Player controller and a World Position (Vector 3) of the object you’re projecting and returns a Screen Position (Vector 2D). The object being projected is a ball. Whenever the ball is behind the player the Screen Position returns a zero vector. I’m not 100% sure why, but the node stops returning a value. I was thinking that it may be due to the fact that the ball at some point is no longer being rendered and thus the function returns a zero vector but this doesn’t make much sense since the world location of the ball is passed through continuously.

Is there an alternative way of calculating the Screen Position for when the object is behind the player?

Here you can see the ball is slightly of screen:

Here once the player has turned so the ball is almost directly behind it the indicator disappears (because the Project World To Screen stops returning a Screen position):



bool UGameplayStatics::ProjectWorldToScreen(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition)
{
	ULocalPlayer* const LP = Player ? Player->GetLocalPlayer() : nullptr;
	if (LP && LP->ViewportClient)
	{
		// get the projection data
		FSceneViewProjectionData ProjectionData;
		if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData))
		{
			FMatrix const ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix();
			return FSceneView::ProjectWorldToScreen(WorldPosition, ProjectionData.GetConstrainedViewRect(), ViewProjectionMatrix, ScreenPosition);
		}
	}

	ScreenPosition = FVector2D::ZeroVector;
	return false;
}


This is the UE4 4.9.2 Build ‘Project World To Screen’ function in C++

The default projection only works for objects that are in front of the camera’s direction vector, and unfortunately there’s no way to alter that in Blueprint.

I wrote a snippet of C++ which can handle reverse projections, which I use for screen-edge arrows, here’s the code. I’m going to put a Wiki tutorial up soon. ‘TargetWorldLocation’ is the position you want to transform.



		FVector Projected;
		bool bTargetBehindCamera = false;

		// Custom Projection Function
		ULocalPlayer* const LP = OwningGESPlayer->GetLocalPlayer();
		if (LP && LP->ViewportClient)
		{
			FSceneViewProjectionData NewProjectionData;
			if (LP->GetProjectionData(LP->ViewportClient->Viewport, EStereoscopicPass::eSSP_FULL, NewProjectionData))
			{
				const FMatrix ViewProjectionMatrix = NewProjectionData.ComputeViewProjectionMatrix();
				const FIntRect ViewRectangle = NewProjectionData.GetConstrainedViewRect();

				FPlane Result = ViewProjectionMatrix.TransformFVector4(FVector4(TargetWorldLocation, 1.f));
				if (Result.W < 0.f) { bTargetBehindCamera = true; }
				if (Result.W == 0.f) { Result.W = 1.f; } // Prevent Divide By Zero

				const float RHW = 1.f / FMath::Abs(Result.W);
				Projected = FVector(Result.X, Result.Y, Result.Z) * RHW;
	
				// Normalize to 0..1 UI Space
 				const float NormX = (Projected.X / 2.f) + 0.5f;
 				const float NormY = 1.f - (Projected.Y / 2.f) - 0.5f;
 
 				Projected.X = (float)ViewRectangle.Min.X + (NormX * (float)ViewRectangle.Width());
 				Projected.Y = (float)ViewRectangle.Min.Y + (NormY * (float)ViewRectangle.Height());
			}
		}

		FVector2D ScreenPos = FVector2D(Projected.X, Projected.Y);


2 Likes

Thank you for firstly fixing my post and also thank you very much for sharing your solution. I wonder if it will be addressed down the line. I will happily merge your function and test it.

I wish I know how to use this =(

I wanted projections from behind the player also, so I dropped more or less what TheJamsh above has into a function that closely matches UE 4.14 ProjectWorldToScreen.

Declared:


UFUNCTION(BlueprintPure, Category = "UI Math")
static bool ProjectWorldToScreenBidirectional(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition, bool& bTargetBehindCamera, bool bPlayerViewportRelative = false);

Defined:


bool UIBlueprintFunctionLibrary::ProjectWorldToScreenBidirectional(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition, bool& bTargetBehindCamera, bool bPlayerViewportRelative)
{
	FVector Projected;
	bool bSuccess = false;

	ULocalPlayer* const LP = Player ? Player->GetLocalPlayer() : nullptr;
	if (LP && LP->ViewportClient)
	{
		// get the projection data
		FSceneViewProjectionData ProjectionData;
		if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData))
		{
			const FMatrix ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix();
			const FIntRect ViewRectangle = ProjectionData.GetConstrainedViewRect();

			FPlane Result = ViewProjectionMatrix.TransformFVector4(FVector4(WorldPosition, 1.f));
			if (Result.W < 0.f) { bTargetBehindCamera = true; }
			if (Result.W == 0.f) { Result.W = 1.f; } // Prevent Divide By Zero

			const float RHW = 1.f / FMath::Abs(Result.W);
			Projected = FVector(Result.X, Result.Y, Result.Z) * RHW;

			// Normalize to 0..1 UI Space
			const float NormX = (Projected.X / 2.f) + 0.5f;
			const float NormY = 1.f - (Projected.Y / 2.f) - 0.5f;

			Projected.X = (float)ViewRectangle.Min.X + (NormX * (float)ViewRectangle.Width());
			Projected.Y = (float)ViewRectangle.Min.Y + (NormY * (float)ViewRectangle.Height());

			bSuccess = true;
			ScreenPosition = FVector2D(Projected.X, Projected.Y);

			if (bPlayerViewportRelative)
			{
				ScreenPosition -= FVector2D(ProjectionData.GetConstrainedViewRect().Min);
			}
		}
		else
		{
			ScreenPosition = FVector2D(1234, 5678);
		}
	}

	return bSuccess;
}

Looks like:

projection_node.png

2 Likes

Can someone please explain, how do I add this to my editor?

You need to create a new C++ class on Unreal Editor as Blueprint Function Library and save,
then you add the first piece of code on the [name of your class].h and the second part on [name of your class].cpp

Save everything and compile on the editor.

I suggest you to add na ELSE parameter to set bTargetBehindCamera back to false when look to the tag point again.

if (Result.W < 0.f) { bTargetBehindCamera = true; } else {bTargetBehindCamera = false;}

Just an update for anyone not getting this to work in UE5.

Code for the .h

UFUNCTION(BlueprintPure, Category = "UI Math")
static bool ProjectWorldToScreenBidirectional(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition, bool& bTargetBehindCamera, bool bPlayerViewportRelative = false);

Code for the .cpp

bool UIBlueprintFunctionLibrary::ProjectWorldToScreenBidirectional(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition, bool& bTargetBehindCamera, bool bPlayerViewportRelative)
{
	FVector Projected;
	bool bSuccess = false;

	ULocalPlayer* const LP = Player ? Player->GetLocalPlayer() : nullptr;
	if (LP && LP->ViewportClient)
	{
		// get the projection data
		FSceneViewProjectionData ProjectionData;
		if (LP->GetProjectionData(LP->ViewportClient->Viewport, /*out*/ ProjectionData))
		{
			const FMatrix ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix();
			const FIntRect ViewRectangle = ProjectionData.GetConstrainedViewRect();

			FPlane Result = ViewProjectionMatrix.TransformFVector4(FVector4(WorldPosition, 1.f));
			if (Result.W < 0.f) { bTargetBehindCamera = true; } else { bTargetBehindCamera = false; }
			if (Result.W == 0.f) { Result.W = 1.f; } // Prevent Divide By Zero

			const float RHW = 1.f / FMath::Abs(Result.W);
			Projected = FVector(Result.X, Result.Y, Result.Z) * RHW;

			// Normalize to 0..1 UI Space
			const float NormX = (Projected.X / 2.f) + 0.5f;
			const float NormY = 1.f - (Projected.Y / 2.f) - 0.5f;

			Projected.X = (float)ViewRectangle.Min.X + (NormX * (float)ViewRectangle.Width());
			Projected.Y = (float)ViewRectangle.Min.Y + (NormY * (float)ViewRectangle.Height());

			bSuccess = true;
			ScreenPosition = FVector2D(Projected.X, Projected.Y);

			if (bPlayerViewportRelative)
			{
				ScreenPosition -= FVector2D(ProjectionData.GetConstrainedViewRect().Min);
			}
		}
		else
		{
			ScreenPosition = FVector2D(1234, 5678);
		}
	}

	return bSuccess;
}

Changed:

if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData))

To

if (LP->GetProjectionData(LP->ViewportClient->Viewport, /*out*/ ProjectionData))

And added Daniel_Alves_br’s suggestion.

I suggest you to add na ELSE parameter to set bTargetBehindCamera back to false when look to the tag point again.

if (Result.W < 0.f) { bTargetBehindCamera = true; } else {bTargetBehindCamera = false;}
3 Likes

@BrandenMarais I seem to be running into a weird issue when the camera is infront of my world position:
https://youtu.be/5PHJEjvI9oI

I am using your function and was wondering if you had any idea on why the screen position would be swaying right when moving infront of the world position? And I was also wondering if I could see how you use the function for off screen indicators?