So, I have something working, which I am going to post some code and blueprint nodes to show what I have done so far. There is a lot of incorrect logic (calculating zoom and FOV wrong, some data might be handled incorrectly, ect) but I have something working.
My pawn class (which is just a base created class from creating a blank project for simulation) contains the following logic for BeginPlay, Left Mouse Button, and Mouse XY 2D-Axis events.
BeginPlay() just gets the current HUD from the player controller, casts it to my C++ class HUD (TowerBaseHUD, see code below), sets the TowerBaseHUD variable in the blueprint and pulls the BinocularsWidget variable from TowerBaseHUD, casts it to the BP_BinocularWidget to get the BinocularRenderTarget variable (which points to a UMG image in BP_BinocularWidget) and sets it to hidden to make the widget not render when beginning play.
OnLeftMouseDown() captures the BP_Binoculars object and calls ::ChangeZoomLevel (ABinoculars, see code below) and if the zoom level is not set to 1 (meaning there is zoom), it will set the BinocularRenderTarget variable as visible, else it will hide it.
OnMouseMoveXY2DAxis() is called each frame to return if the mouse has moved from the previous frame (I believe) and I check if any axis has a vector value on both axes. If so, I capture the viewport mouse position, ABinoculars object, and BinocularsWidget object then call ABinoculars::ChangeCameraRotation and ATower3DPawn::UpdateBinocularWidget (see code below) updating the widget based on the passed FVector2D of the mouse position.
This is a simple AHUD class that assigns the HUD to the viewport, which is based upon the level at the moment (World Settings::GameMode tab for the level). Not much here.
This is my UMG widget. The thing is set to fill the screen. The Border is just set to whatever pixel size I want the texture to be on the UI. The BinocularRenderTarget is set to match the Image (which is a BP_RenderTarget in my content, which the BP_Binoculars is rendering to). So, while the render target size might enough pixels for whatever resolution I want to support, I can dynamically perform a ResizeTarget on it to render in a lower resolution. Also, I can resize the Border in the widget to get the output to match whatever size I need it to fill on the screen.
ABinoculars.h:
#pragma once
#include "CoreMinimal.h"
#include "Engine/SceneCapture2D.h"
#include "Tower3DGameInstance.h"
#include "Binoculars.generated.h"
class UObjectLibrary;
class UMaterialInstanceConstant;
/**
*
*/
UCLASS()
class TOWER3D_API ABinoculars : public ASceneCapture2D
{
GENERATED_BODY()
float currentHorizontalFOV;
float currentViewportX;
float currentVerticalFOV;
float currentViewportY;
float aspectRatio;
FVector PerZoomLevelFOV;
void CalculateBinocularsData();
FVector2D GetNDisplayViewportSize() const;
void GenerateNDisplayCamera();
FVector2D GetNormalViewportSize() const;
void GenerateNormalCamera();
void OnViewportResized(FViewport* Viewport, uint32 Unused);
void OnViewportToggleFullscreen(bool IsFullScreen);
TObjectPtr<USceneCaptureComponent2D> m_SceneCaptureComponent2D;
public:
ABinoculars();
UPROPERTY(BlueprintReadOnly)
int ZoomLevel;
UPROPERTY(BlueprintReadWrite)
TObjectPtr<UUserWidget> BinocularUI;
UPROPERTY(BlueprintReadOnly)
TObjectPtr<UTower3DGameInstance> GameInstance;
UFUNCTION(BlueprintCallable)
void ChangeZoomLevel();
UFUNCTION(BlueprintCallable)
void ChangeCameraRotation(FVector2D MouseViewportPosition);
virtual void BeginPlay() override;
};
ABinoculars.cpp:
#include "Binoculars.h"
#include "TowerBaseHUD.h"
#include "Engine.h"
ABinoculars::ABinoculars()
: currentHorizontalFOV(0.0)
, currentViewportX(0.0)
, currentVerticalFOV(0.0)
, currentViewportY(0.0)
, aspectRatio(0.0)
, ZoomLevel(0)
{
}
void ABinoculars::CalculateBinocularsData()
{
if(!BinocularUI)
{
m_SceneCaptureComponent2D = GetCaptureComponent2D();
APlayerController* playerController = GetWorld()->GetFirstPlayerController();
if(playerController)
{
ATowerBaseHUD* playerHUD = playerController->GetHUD<ATowerBaseHUD>();
BinocularUI = playerHUD->BinocularWidget;
currentHorizontalFOV = playerController->PlayerCameraManager->GetFOVAngle();
// the Unreal Engine defaults to a static horizontal FOV, thus the aspect ratio can change but the horizontal FOV will be maintained
// this will make the vertical FOV change as the aspect ratio changes
currentVerticalFOV = currentHorizontalFOV / aspectRatio;
}
}
}
FVector2D ABinoculars::GetNDisplayViewportSize() const
{
UGameUserSettings* pGameUserSettings = UGameUserSettings::GetGameUserSettings();
return FVector2D(pGameUserSettings->GetScreenResolution().X, pGameUserSettings->GetScreenResolution().Y);
}
void ABinoculars::GenerateNDisplayCamera()
{
FVector2D viewport(GetNDisplayViewportSize());
currentViewportX = viewport.X;
currentViewportY = viewport.Y;
aspectRatio = viewport.X / viewport.Y;
}
FVector2D ABinoculars::GetNormalViewportSize() const
{
FVector2D vecViewport;
GetWorld()->GetGameViewport()->GetViewportSize(vecViewport);
return vecViewport;
}
void ABinoculars::GenerateNormalCamera()
{
FVector2D viewport(GetNormalViewportSize());
currentViewportX = viewport.X;
currentViewportY = viewport.Y;
aspectRatio = viewport.X / viewport.Y;
}
void ABinoculars::OnViewportResized(FViewport* Viewport, uint32 Unused)
{
if(GameInstance->bIsNDisplay)
{
GenerateNDisplayCamera();
}
else
{
GenerateNormalCamera();
}
CalculateBinocularsData();
}
void ABinoculars::OnViewportToggleFullscreen(bool IsFullScreen)
{
if(GameInstance->bIsNDisplay)
{
GenerateNDisplayCamera();
}
else
{
GenerateNormalCamera();
}
CalculateBinocularsData();
}
void ABinoculars::ChangeZoomLevel()
{
// swap between the zoom levels and change the field of view to match the zoom level
switch(ZoomLevel)
{
case 1:
{
if(m_SceneCaptureComponent2D)
{
m_SceneCaptureComponent2D->FOVAngle = 45.0;
}
ZoomLevel = 2;
break;
}
case 2:
{
if(m_SceneCaptureComponent2D)
{
m_SceneCaptureComponent2D->FOVAngle = 30.0;
}
ZoomLevel = 3;
break;
}
case 3:
{
if(m_SceneCaptureComponent2D)
{
m_SceneCaptureComponent2D->FOVAngle = 22.5;
}
ZoomLevel = 4;
break;
}
case 4:
{
if(m_SceneCaptureComponent2D)
{
m_SceneCaptureComponent2D->FOVAngle = 18.0;
}
ZoomLevel = 5;
break;
}
case 5:
{
if(m_SceneCaptureComponent2D)
{
m_SceneCaptureComponent2D->FOVAngle = 90.0;
}
ZoomLevel = 1;
break;
}
default:
{
if(m_SceneCaptureComponent2D)
{
m_SceneCaptureComponent2D->FOVAngle = 90.0;
}
ZoomLevel = 1;
break;
}
}
}
void ABinoculars::ChangeCameraRotation(FVector2D MouseViewportPosition)
{
FRotator cameraRotation = GetActorRotation();
cameraRotation.Yaw = (MouseViewportPosition.X / currentViewportX) * currentHorizontalFOV;
cameraRotation.Pitch = (MouseViewportPosition.Y / currentViewportY) * currentVerticalFOV;
SetActorRotation(cameraRotation);
}
void ABinoculars::BeginPlay()
{
Super::BeginPlay();
GameInstance = Cast<UTower3DGameInstance>(GetGameInstance());
GEngine->GameViewport->OnToggleFullscreen().AddUObject(this, &ABinoculars::OnViewportToggleFullscreen);
GEngine->GameViewport->Viewport->ViewportResizedEvent.AddUObject(this, &ABinoculars::OnViewportResized);
ChangeZoomLevel();
}
You can probably see from my initial post, there is a lot less code, but it’s much cleaner and the ideas are more consistent with what I am attempting to do. This also places a lot of gameplay logic into blueprint nodes with more finer control within functions of the class. I am not certain if this is a good idea, and there is part of me that feels like I can implement ALL logic within C++, but I don’t want to fight the editor and code if this gets me what I want. This class will compile and work but is incomplete, as it doesn’t handle various states correctly, but it’s a starting point.
TowerBaseHUD.h:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "Blueprint/UserWidget.h"
#include "TowerBaseHUD.generated.h"
/**
*
*/
UCLASS()
class TOWER3D_API ATowerBaseHUD : public AHUD
{
GENERATED_BODY()
public:
UPROPERTY(BlueprintReadWrite)
TObjectPtr<UUserWidget> BinocularWidget;
};
This class is very small, as it is only utilized to expose the binocular widget to blueprints so the logic can be connected via other blueprints (namely the pawn class).
Tower3DPawn.h:
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/DefaultPawn.h"
#include "Blueprint/UserWidget.h"
#include "Tower3DPawn.generated.h"
/**
*
*/
UCLASS()
class TOWER3D_API ATower3DPawn : public ADefaultPawn
{
GENERATED_BODY()
public:
UFUNCTION(BlueprintCallable)
void UpdateBinocularWidget(UUserWidget* BinocularWidget, FVector2D MouseViewportPosition);
};
Tower3DPawn.cpp:
#include "Tower3DPawn.h"
#include "TowerBaseHUD.h"
#include "Blueprint/UserWidget.h"
void ATower3DPawn::UpdateBinocularWidget(UUserWidget* BinocularWidget, FVector2D MouseViewportPosition)
{
if(BinocularWidget)
{
BinocularWidget->SetPositionInViewport(MouseViewportPosition);
}
}
While small, finding this function took me forever to find. I had to spend many hours and lots of trial code to condense down a way to just set the widget to the mouse position. So, this is all it took.
Basically, the picture you have above, have that but when you move the mouse, not only does PIP window moves with the mouse, where it is zooming in the world is also moving. Think of literally placing a magnifying glass onto your monitor, assume you could actually peer into the world, and it’s zooming.
But, thanks for all the input you had given me to get to this point. I am kind of worried how this will interact with nDisplay, but, again, that is beyond the scope of this thread.
Thank you very kindly, nande.