Skip to content

Instantly share code, notes, and snippets.

@TheEnbyWitch
Last active December 13, 2024 01:02
Show Gist options
  • Save TheEnbyWitch/f9d4fd9efe4f54ca0bebcf3036720664 to your computer and use it in GitHub Desktop.
Save TheEnbyWitch/f9d4fd9efe4f54ca0bebcf3036720664 to your computer and use it in GitHub Desktop.
Custom Aspect Ratio Axis Constraint behavior in a LocalPlayer subclass
// 2023 girlfriend games
#include "Player/PlatformerLocalPlayer.h"
#include "SceneViewExtension.h"
#include "SceneViewExtensionContext.h"
#include "UI/PlatformerViewportClient.h"
int32 LocalCalcLocalPlayerCachedLODDistanceFactor = 1;
static FAutoConsoleVariableRef LocalCVarCalcLocalPlayerCachedLODDistanceFactor(
TEXT("r.CalcLocalPlayerCachedLODDistanceFactor"),
LocalCalcLocalPlayerCachedLODDistanceFactor,
TEXT("Should we calculate a LOD Distance Factor based on the current FOV. Should not be necessary since LOD is already based on screen size.\n")
);
void CalculateProjectionMatrixGivenView(const FMinimalViewInfo& ViewInfo, TEnumAsByte<enum EAspectRatioAxisConstraint> AspectRatioAxisConstraint, FViewport* Viewport, FSceneViewProjectionData& InOutProjectionData)
{
// Create the projection matrix (and possibly constrain the view rectangle)
if (ViewInfo.bConstrainAspectRatio)
{
FIntRect ConstrainedViewRect = Viewport->CalculateViewExtents(ViewInfo.AspectRatio, InOutProjectionData.GetViewRect());
// Enforce a particular aspect ratio for the render of the scene.
// Results in black bars at top/bottom etc.
/*
int32 BlackBarHeight = ConstrainedViewRect.Min.Y;
int32 MoveBy = BlackBarHeight / 2;
ConstrainedViewRect.Min.Y -= MoveBy;
ConstrainedViewRect.Max.Y -= MoveBy;
*/
InOutProjectionData.SetConstrainedViewRectangle(ConstrainedViewRect);
InOutProjectionData.ProjectionMatrix = ViewInfo.CalculateProjectionMatrix();
}
else
{
float XAxisMultiplier;
float YAxisMultiplier;
/*
FIntRect NewViewRect = InOutProjectionData.GetViewRect();
// 4/3 -> 16/9
// 12/9 -> 16/9
// 1.333333333333333 -> 1.777777777777778
int32 ViewHeight = NewViewRect.Height();
NewViewRect.Min.Y += ViewHeight / 9;
NewViewRect.Max.Y -= ViewHeight*2 / 9;
InOutProjectionData.SetViewRectangle(NewViewRect);*/
const FIntRect& ViewRect = InOutProjectionData.GetViewRect();
const int32 SizeX = ViewRect.Width();
const int32 SizeY = ViewRect.Height();
AspectRatioAxisConstraint = AspectRatio_MaintainYFOV;
// Aspect ratio will work like this:
// 1) Check if screen width is smaller than screen height * aspect ratio
// 2) if yes, work like AspectRatio_MaintainXFOV, MaintainYFOV if not
// this actually will work based on the Aspect Ratio you set on your camera component!
const bool bMaintainXFOV =
((SizeX < (SizeY * ViewInfo.AspectRatio)) && (AspectRatioAxisConstraint == AspectRatio_MaintainYFOV ||
(AspectRatioAxisConstraint == AspectRatio_MaintainXFOV))) ||
(ViewInfo.ProjectionMode == ECameraProjectionMode::Orthographic);
if (bMaintainXFOV)
{
// If the viewport is wider than it is tall
XAxisMultiplier = 1.0f;
YAxisMultiplier = SizeX / (float)SizeY;
}
else
{
// If the viewport is taller than it is wide
XAxisMultiplier = SizeY / (float)SizeX;
YAxisMultiplier = 1.0f;
}
float MatrixHalfFOV;
if (!bMaintainXFOV && ViewInfo.AspectRatio != 0.f)
{
// The view-info FOV is horizontal. But if we have a different aspect ratio constraint, we need to
// adjust this FOV value using the aspect ratio it was computed with, so we that we can compute the
// complementary FOV value (with the *effective* aspect ratio) correctly.
const float HalfXFOV = FMath::DegreesToRadians(FMath::Max(0.001f, ViewInfo.FOV) / 2.f);
const float HalfYFOV = FMath::Atan(FMath::Tan(HalfXFOV) / ViewInfo.AspectRatio);
MatrixHalfFOV = HalfYFOV;
}
else
{
// Avoid divide by zero in the projection matrix calculation by clamping FOV.
// Note the division by 360 instead of 180 because we want the half-FOV.
MatrixHalfFOV = FMath::Max(0.001f, ViewInfo.FOV) * (float)UE_PI / 360.0f;
}
if (ViewInfo.ProjectionMode == ECameraProjectionMode::Orthographic)
{
const float OrthoWidth = ViewInfo.OrthoWidth / 2.0f * XAxisMultiplier;
const float OrthoHeight = (ViewInfo.OrthoWidth / 2.0f) / YAxisMultiplier;
const float NearPlane = ViewInfo.OrthoNearClipPlane;
const float FarPlane = ViewInfo.OrthoFarClipPlane;
const float ZScale = 1.0f / (FarPlane - NearPlane);
const float ZOffset = -NearPlane;
InOutProjectionData.ProjectionMatrix = FReversedZOrthoMatrix(
OrthoWidth,
OrthoHeight,
ZScale,
ZOffset
);
}
else
{
const float ClippingPlane = ViewInfo.GetFinalPerspectiveNearClipPlane();
InOutProjectionData.ProjectionMatrix = FReversedZPerspectiveMatrix(
MatrixHalfFOV,
MatrixHalfFOV,
XAxisMultiplier,
YAxisMultiplier,
ClippingPlane,
ClippingPlane
);
}
}
if (!ViewInfo.OffCenterProjectionOffset.IsZero())
{
const float Left = -1.0f + ViewInfo.OffCenterProjectionOffset.X;
const float Right = Left + 2.0f;
const float Bottom = -1.0f + ViewInfo.OffCenterProjectionOffset.Y;
const float Top = Bottom + 2.0f;
InOutProjectionData.ProjectionMatrix.M[2][0] = (Left + Right) / (Left - Right);
InOutProjectionData.ProjectionMatrix.M[2][1] = (Bottom + Top) / (Bottom - Top);
}
}
extern TAutoConsoleVariable<float> GViewportCutsceneBarOffset;
bool UPlatformerLocalPlayer::GetProjectionData(FViewport* Viewport, FSceneViewProjectionData& ProjectionData,
int32 StereoViewIndex) const
{
if ((Viewport == nullptr) || (PlayerController == nullptr) || (Viewport->GetSizeXY().X == 0) || (Viewport->GetSizeXY().Y == 0) || (Size.X == 0) || (Size.Y == 0))
{
return false;
}
int32 X = FMath::TruncToInt(Origin.X * Viewport->GetSizeXY().X);
int32 Y = FMath::TruncToInt(Origin.Y * Viewport->GetSizeXY().Y);
X += Viewport->GetInitialPositionXY().X;
Y += Viewport->GetInitialPositionXY().Y;
uint32 SizeX = FMath::TruncToInt(Size.X * Viewport->GetSizeXY().X);
uint32 SizeY = FMath::TruncToInt(Size.Y * Viewport->GetSizeXY().Y);
FIntRect UnconstrainedRectangle = FIntRect(X, Y, X+SizeX, Y+SizeY);
ProjectionData.SetViewRectangle(UnconstrainedRectangle);
// Get the viewpoint.
FMinimalViewInfo ViewInfo;
GetViewPoint(/*out*/ ViewInfo);
// If stereo rendering is enabled, update the size and offset appropriately for this pass
const bool bNeedStereo = false;
// scale distances for cull distance purposes by the ratio of our current FOV to the default FOV
if (LocalCalcLocalPlayerCachedLODDistanceFactor != 0)
{
PlayerController->LocalPlayerCachedLODDistanceFactor = ViewInfo.FOV / FMath::Max<float>(0.01f, (PlayerController->PlayerCameraManager != nullptr) ? PlayerController->PlayerCameraManager->DefaultFOV : 90.f);
}
else // This should be removed in the final version. Leaving in so this can be toggled on and off in order to evaluate it.
{
PlayerController->LocalPlayerCachedLODDistanceFactor = 1.f;
}
FVector StereoViewLocation = ViewInfo.Location;
// Create the view matrix
ProjectionData.ViewOrigin = StereoViewLocation;
ProjectionData.ViewRotationMatrix = FInverseRotationMatrix(ViewInfo.Rotation) * FMatrix(
FPlane(0, 0, 1, 0),
FPlane(1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, 0, 1));
if(auto PlatformerViewportClient = Cast<UPlatformerViewportClient>(ViewportClient.Get()))
{
if(PlatformerViewportClient->CutsceneBarAlpha > 0)
ViewInfo.OffCenterProjectionOffset = FVector2D(0, 2*GViewportCutsceneBarOffset.GetValueOnGameThread() * -PlatformerViewportClient->CutsceneBarAlpha);
}
// viewext this use case needs to be revisited
if (!bNeedStereo)
{
// Create the projection matrix (and possibly constrain the view rectangle)
CalculateProjectionMatrixGivenView(ViewInfo, AspectRatioAxisConstraint, Viewport, /*inout*/ ProjectionData);
for (auto& ViewExt : GEngine->ViewExtensions->GatherActiveExtensions(FSceneViewExtensionContext(Viewport)))
{
ViewExt->SetupViewProjectionMatrix(ProjectionData);
};
}
return true;
}
// 2024 girlfriend games
#pragma once
#include "CoreMinimal.h"
#include "Engine/LocalPlayer.h"
#include "PlatformerLocalPlayer.generated.h"
/**
*
*/
UCLASS(Blueprintable)
class PLATFORMERGAME_API UPlatformerLocalPlayer : public ULocalPlayer
{
GENERATED_BODY()
public:
virtual bool GetProjectionData(FViewport* Viewport, FSceneViewProjectionData& ProjectionData,
int32 StereoViewIndex) const override;
};

This is a custom LocalPlayer class that overrides the behaviour of the aspect ratio axis mode to work kinda like the "Maintain Minor Axis" mode, but it takes into account whatever Aspect Ratio you have set on the camera.

This overrides the Override Aspect Ratio Axis setting completely and does not require any .ini edits. Also, no stereo/VR/XR support, this is purely for flat 2D screens.

What it does is if (ScreenWidth < ScreenHeight * AspectRatio), then it will do the same behaviour as the AspectRatio_MaintainXFOV, making it so that your 16:9 (or whatever Aspect Ratio you set on the camera) image is maintained regardless if the window is taller/your phone is in portrait mode/you put the game window to the side to have focus on something else while you play it, while you still get the intended game experience.

When you're using an ultrawide screen (or you have a very wide window), you will get a wider horizontal field of view (like AspectRatio_MaintainYFOV), while keeping the vertical FOV exactly the same.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment