a

Lorem ipsum dolor sit amet, elit eget consectetuer adipiscing aenean dolor

Image Alt

Geodesic Games

Projection Matrices in Unreal Engine

Camera projection is an important topic when talking about rendering and game engines. Projections can completely change a visual experience and can provide much more rendering control for graphics developers.

In Unreal Engine there is little documentation or support in terms of projection matrix calculations which is why we would like to provide you some more information on how Unreal Engine handles its projections.

The purpose of this tutorial is to really break down projection modification to the bare bones to provide you guys with a simple guide to achieving a single goal, changing the local player projection matrix.

We have created a very simple plugin if you would like to download it and follow along: https://github.com/GeodesicGames/SimpleOffAxisProjection

Unreal vs OpenGL (Unity) Projections

Unreal handles projections differently than standard OpenGL perspective matrix as used in Unity.

Firstly, Unreal inverses the perspective divide, applying 1 instead of -1 for the “W” value.

Below is the matrix taken from Unity camera.perspectiveMatrix. By default it is -1.0 but I have flipped it to 1.0 for demonstration purposes. You can see Unity documentation at the link below.

https://docs.unity3d.com/ScriptReference/Camera-projectionMatrix.html

static Matrix4x4 PerspectiveOffCenter(float left, float right, float bottom, float top, float near, float far)
{
	float x = 2.0F * near / (right - left);
	float y = 2.0F * near / (top - bottom);
	float a = (right + left) / (right - left);
	float b = (top + bottom) / (top - bottom);
	float c = -(far + near) / (far - near);
	float d = -(2.0F * far * near) / (far - near);
	float e = -1.0F; // Needs to be flipped for UNREAL to 1.0F
	Matrix4x4 m = new Matrix4x4();
	m[0, 0] = x;
	m[0, 1] = 0;
	m[0, 2] = a;
	m[0, 3] = 0;
	m[1, 0] = 0;
	m[1, 1] = y;
	m[1, 2] = b;
	m[1, 3] = 0;
	m[2, 0] = 0;
	m[2, 1] = 0;
	m[2, 2] = c;
	m[2, 3] = d;
	m[3, 0] = 0;
	m[3, 1] = 0;
	m[3, 2] = e;
	m[3, 3] = 0;
	return m;
}

Secondly, Unreal applies a matrix transposition to all their perspective matrices.

Matrix Transposition:

Let’s start looking at how Unreal defines its various forms of projections.

Perspective Matrix in Unreal Engine

By default Unreal provides the PerspectiveMatrix class with various handy perspective matrix constructors. There are two variation, a normal PerspectiveMatrix and ReversedZPerspectiveMatrix. 

The basic point of this technique lies in swapping Far and Near values in projection matrix. When depth is calculated for a standard matrix, an addition of numbers of different orders may occur (for example, 50000 and 0.5). This operation is quite heavy due to specifics floating-point numbers representation. On the other hand, when reversed projection matrix is used, this behavior occurs only in the area, that is close to the camera. Thus, it is more effective.” – Andrii Melnyk ( https://answers.unrealengine.com/users/21854/view.html )


FPerspectiveMatrix ( float HalfFOV, float Width, float Height, float MinZ ) 
FPerspectiveMatrix ( float HalfFOV, float Width, float Height, float MinZ, float MaxZ ) 
FPerspectiveMatrix ( float HalfFOVX, float HalfFOVY, float MultFOVX, float MultFOVY, float MinZ, float MaxZ )

FReversedZPerspectiveMatrix ( float HalfFOV, float Width, float Height, float MinZ ) 
FReversedZPerspectiveMatrix ( float HalfFOV, float Width, float Height, float MinZ, float MaxZ ) 
FReversedZPerspectiveMatrix ( float HalfFOVX, float HalfFOVY, float MultFOVX, float MultFOVY, float MinZ, float MaxZ )


https://api.unrealengine.com/INT/API/Runtime/Core/Math/FPerspectiveMatrix/index.html

Header Path: Engine\Source\Runtime\Core\Public\Math\PerspectiveMatrix.h

 

Let’s take a look at how these are constructed.


FORCEINLINE FPerspectiveMatrix::FPerspectiveMatrix(float HalfFOV, float Width, float Height, float MinZ, float MaxZ)
: FMatrix(
FPlane(1.0f / FMath::Tan(HalfFOV), 0.0f, 0.0f, 0.0f),
FPlane(0.0f, Width / FMath::Tan(HalfFOV) / Height, 0.0f, 0.0f),
FPlane(0.0f, 0.0f, ((MinZ == MaxZ) ? (1.0f - Z_PRECISION) : MaxZ / (MaxZ - MinZ)), 1.0f),
FPlane(0.0f, 0.0f, -MinZ * ((MinZ == MaxZ) ? (1.0f - Z_PRECISION) : MaxZ / (MaxZ - MinZ)), 0.0f)
)
{ }

Comparing the two side by side, we get:

Unreal EngineValueUnityValue
[0,0]1.0f / FMath::Tan(HalfFOV)[0,0]2.0F * near / (right - left);
[1,1]Width / FMath::Tan(HalfFOV) / Height[1,1]2.0F * near / (top - bottom);
[2,0]0.0F[0,2](right + left) / (right - left);
[2,1]0.0F[1,2](top + bottom) / (top - bottom);
[2,2]((MinZ == MaxZ) ? (1.0f - Z_PRECISION) : MaxZ / (MaxZ - MinZ))[2,2]-(far + near) / (far - near);
[3,2]-MinZ * ((MinZ == MaxZ) ? (1.0f - Z_PRECISION) : MaxZ / (MaxZ - MinZ[2,3]-(2.0F * far * near) / (far - near);
[2,3]1.0f[3,2]-1.0f

Alright now that we understand the differences between projection matrices in Unreal and Unity, lets take a look at how we can go about actually getting and modifying the projection matrices.

 

Getting and Setting Local Player Projection Matrix

There are a few simple steps we need to follow in order to change the LocalPlayer projection matrix.

First, lets create a new C++ class that inherits from LocalPlayer as seen below.

Right click in content browser -> Add New C++ Class -> Show All Classes -> Select Local Player

 

Once your new local player class is compiled we can start to add some code.

Navigate to your header and add the following under your GENERATED_BODY()

 


FSceneView* CalcSceneView(class FSceneViewFamily* ViewFamily,
		FVector& OutViewLocation,
		FRotator& OutViewRotation,
		FViewport* Viewport,
		class FViewElementDrawer* ViewDrawer = NULL,
		EStereoscopicPass StereoPass = eSSP_FULL) override;

This will override the ULocalPlayer CalcSceneView which is necessary for getting the current scene view.

Next lets go into the .cpp file and add our logic for getting and setting our new projection:

 

FSceneView * UOffAxisLocalPlayer::CalcSceneView(FSceneViewFamily * ViewFamily, FVector &OutViewLocation, FRotator &OutViewRotation, FViewport * Viewport, FViewElementDrawer * ViewDrawer, EStereoscopicPass StereoPass)
{

	FSceneView* View = ULocalPlayer::CalcSceneView(ViewFamily, OutViewLocation, OutViewRotation, Viewport, ViewDrawer, StereoPass);

	if (View)
	{
		FMatrix CurrentMatrix = View->ViewMatrices.GetProjectionMatrix();

		float FOV = FMath::DegreesToRadians(60.0f);
		FMatrix ProjectionMatrix = FReversedZPerspectiveMatrix(FOV, 16.0f, 9.0f, GNearClippingPlane);

		View->UpdateProjectionMatrix(ProjectionMatrix);
	}

	return View;
}

In our code, we can get the current matrix with:

View->ViewMatrices.GetProjectionMatrix();

We can set it with the following:

View->UpdateProjectionMatrix(FMatrix::Identity);

In this case I will be using one of the built in Unreal Functions provided by ProjectionMatrix.h.

If you want to do the same, make sure you #include ProjectionMatrix.h

 

Our last step is to run the project and set our new default LocalPlayer class.

To do that, go to Project Settings -> General Settings -> Local Player Class

Click the drop down and select your custom LocalPlayer class.

 

Finally, restart the editor to apply your LocalPlayer changes and thats it!

Upon play, you should see your newly applied projection.

 

Final Result

 

OffAxisLocalPlayer.h


#pragma once

#include "CoreMinimal.h"
#include "Engine/LocalPlayer.h"
#include "OffAxisLocalPlayer.generated.h"

/**
*
*/
UCLASS(BlueprintType)
class OFFAXISPROJECTION_API UOffAxisLocalPlayer : public ULocalPlayer
{
GENERATED_BODY()

FSceneView* CalcSceneView(class FSceneViewFamily* ViewFamily,
FVector& OutViewLocation,
FRotator& OutViewRotation,
FViewport* Viewport,
class FViewElementDrawer* ViewDrawer = NULL,
EStereoscopicPass StereoPass = eSSP_FULL) override;

};

 

OffAxisLocalPlayer.cpp

 


#include "OffAxisLocalPlayer.h"
#include "PerspectiveMatrix.h"

FSceneView * UOffAxisLocalPlayer::CalcSceneView(FSceneViewFamily * ViewFamily, FVector & OutViewLocation, FRotator & OutViewRotation, FViewport * Viewport, FViewElementDrawer * ViewDrawer, EStereoscopicPass StereoPass)
{

FSceneView* View = ULocalPlayer::CalcSceneView(ViewFamily, OutViewLocation, OutViewRotation, Viewport, ViewDrawer, StereoPass);

if (View)
{
FMatrix CurrentMatrix = View->ViewMatrices.GetProjectionMatrix();

float FOV = FMath::DegreesToRadians(60.0f);
FMatrix ProjectionMatrix = FReversedZPerspectiveMatrix(FOV, 16.0f, 9.0f, GNearClippingPlane);

View->UpdateProjectionMatrix(ProjectionMatrix);
}

return View;
}

 

Download the plugin here: https://github.com/GeodesicGames/SimpleOffAxisProjection

 

If you are interested in a deeper dive into this or would like to know more about the theory behind projection matrices you can see the links below.

OffAxisProjection Plugin: https://github.com/fweidner/UE4-Plugin-OffAxis

Resource for theory and math: https://www.scratchapixel.com/lessons/3d-basic-rendering/perspective-and-orthographic-projection-matrix/building-basic-perspective-projection-matrix