UE4 View and Projection Matrix
UE4 的透视投影矩阵比较特殊。以下分析Game模式和Editor模式下每一帧投影矩阵和视图矩阵的相关处理过程。
Game 模式
Tick
UGameViewportClient::Draw
ULocalPlayer::CalcSceneView
ULocalPlayer::GetProjectionData
FMinimalViewInfo::CalculateProjectionMatrixGivenView
void UGameViewportClient::Draw(FViewport* InViewport, FCanvas* SceneCanvas){
//
FSceneView* View = LocalPlayer->CalcSceneView(&ViewFamily, ViewLocation, ViewRotation, InViewport, &GameViewDrawer, PassType);
//
}
FSceneView* ULocalPlayer::CalcSceneView( class FSceneViewFamily* ViewFamily,
FVector& OutViewLocation,
FRotator& OutViewRotation,
FViewport* Viewport,
class FViewElementDrawer* ViewDrawer,
EStereoscopicPass StereoPass)
{
if ((PlayerController == NULL) || (Size.X <= 0.f) || (Size.Y <= 0.f) || (Viewport == NULL))
{
return NULL;
}
FSceneViewInitOptions ViewInitOptions;
// get the projection data
if (GetProjectionData(Viewport, StereoPass, /*inout*/ ViewInitOptions) == false)
{
// Return NULL if this we didn't get back the info we needed
return NULL;
}
// return if we have an invalid view rect
if (!ViewInitOptions.IsValidViewRectangle())
{
return NULL;
}
// Get the viewpoint...technically doing this twice
// but it makes GetProjectionData better
FMinimalViewInfo ViewInfo;
GetViewPoint(ViewInfo, StereoPass);
OutViewLocation = ViewInfo.Location;
OutViewRotation = ViewInfo.Rotation;
if (PlayerController->PlayerCameraManager != NULL)
{
// Apply screen fade effect to screen.
if (PlayerController->PlayerCameraManager->bEnableFading)
{
ViewInitOptions.OverlayColor = PlayerController->PlayerCameraManager->FadeColor;
ViewInitOptions.OverlayColor.A = FMath::Clamp(PlayerController->PlayerCameraManager->FadeAmount,0.0f,1.0f);
}
// Do color scaling if desired.
if (PlayerController->PlayerCameraManager->bEnableColorScaling)
{
ViewInitOptions.ColorScale = FLinearColor(
PlayerController->PlayerCameraManager->ColorScale.X,
PlayerController->PlayerCameraManager->ColorScale.Y,
PlayerController->PlayerCameraManager->ColorScale.Z
);
}
// Was there a camera cut this frame?
ViewInitOptions.bInCameraCut = PlayerController->PlayerCameraManager->bGameCameraCutThisFrame;
}
check(PlayerController && PlayerController->GetWorld());
// Fill out the rest of the view init options
ViewInitOptions.ViewFamily = ViewFamily;
ViewInitOptions.SceneViewStateInterface = ((StereoPass != eSSP_RIGHT_EYE) ? ViewState.GetReference() : StereoViewState.GetReference());
ViewInitOptions.ViewActor = PlayerController->GetViewTarget();
ViewInitOptions.ViewElementDrawer = ViewDrawer;
ViewInitOptions.BackgroundColor = FLinearColor::Black;
ViewInitOptions.LODDistanceFactor = PlayerController->LocalPlayerCachedLODDistanceFactor;
ViewInitOptions.StereoPass = StereoPass;
ViewInitOptions.WorldToMetersScale = PlayerController->GetWorldSettings()->WorldToMeters;
ViewInitOptions.CursorPos = Viewport->HasMouseCapture() ? FIntPoint(-1, -1) : FIntPoint(Viewport->GetMouseX(), Viewport->GetMouseY());
ViewInitOptions.bOriginOffsetThisFrame = PlayerController->GetWorld()->bOriginOffsetThisFrame;
ViewInitOptions.bUseFieldOfViewForLOD = ViewInfo.bUseFieldOfViewForLOD;
PlayerController->BuildHiddenComponentList(OutViewLocation, /*out*/ ViewInitOptions.HiddenPrimitives);
FSceneView* const View = new FSceneView(ViewInitOptions);
View->ViewLocation = OutViewLocation;
View->ViewRotation = OutViewRotation;
//@TODO: SPLITSCREEN: This call will have an issue with splitscreen, as the show flags are shared across the view family
EngineShowFlagOrthographicOverride(View->IsPerspectiveProjection(), ViewFamily->EngineShowFlags);
ViewFamily->Views.Add(View);
{
View->StartFinalPostprocessSettings(OutViewLocation);
// CameraAnim override
if (PlayerController->PlayerCameraManager)
{
TArray<FPostProcessSettings> const* CameraAnimPPSettings;
TArray<float> const* CameraAnimPPBlendWeights;
PlayerController->PlayerCameraManager->GetCachedPostProcessBlends(CameraAnimPPSettings, CameraAnimPPBlendWeights);
for (int32 PPIdx = 0; PPIdx < CameraAnimPPBlendWeights->Num(); ++PPIdx)
{
View->OverridePostProcessSettings( (*CameraAnimPPSettings)[PPIdx], (*CameraAnimPPBlendWeights)[PPIdx]);
}
}
// CAMERA OVERRIDE
// NOTE: Matinee works through this channel
View->OverridePostProcessSettings(ViewInfo.PostProcessSettings, ViewInfo.PostProcessBlendWeight);
View->EndFinalPostprocessSettings(ViewInitOptions);
}
for (int ViewExt = 0; ViewExt < ViewFamily->ViewExtensions.Num(); ViewExt++)
{
ViewFamily->ViewExtensions[ViewExt]->SetupView(*ViewFamily, *View);
}
return View;
}
bool ULocalPlayer::GetProjectionData(FViewport* Viewport, EStereoscopicPass StereoPass, FSceneViewProjectionData& ProjectionData) const
{
// If the actor
if ((Viewport == NULL) || (PlayerController == NULL) || (Viewport->GetSizeXY().X == 0) || (Viewport->GetSizeXY().Y == 0))
{
return false;
}
int32 X = FMath::TruncToInt(Origin.X * Viewport->GetSizeXY().X);
int32 Y = FMath::TruncToInt(Origin.Y * Viewport->GetSizeXY().Y);
uint32 SizeX = FMath::TruncToInt(Size.X * Viewport->GetSizeXY().X);
uint32 SizeY = FMath::TruncToInt(Size.Y * Viewport->GetSizeXY().Y);
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
// We expect some size to avoid problems with the view rect manipulation
if(SizeX > 50 && SizeY > 50)
{
int32 Value = CVarViewportTest.GetValueOnGameThread();
if(Value)
{
int InsetX = SizeX / 4;
int InsetY = SizeY / 4;
// this allows to test various typical view port situations (todo: split screen)
switch(Value)
{
case 1: X += InsetX; Y += InsetY; SizeX -= InsetX * 2; SizeY -= InsetY * 2;break;
case 2: Y += InsetY; SizeY -= InsetY * 2; break;
case 3: X += InsetX; SizeX -= InsetX * 2; break;
case 4: SizeX /= 2; SizeY /= 2; break;
case 5: SizeX /= 2; SizeY /= 2; X += SizeX; break;
case 6: SizeX /= 2; SizeY /= 2; Y += SizeY; break;
case 7: SizeX /= 2; SizeY /= 2; X += SizeX; Y += SizeY; break;
}
}
}
#endif
FIntRect UnconstrainedRectangle = FIntRect(X, Y, X+SizeX, Y+SizeY);
ProjectionData.SetViewRectangle(UnconstrainedRectangle);
// Get the viewpoint.
FMinimalViewInfo ViewInfo;
GetViewPoint(/*out*/ ViewInfo, StereoPass);
// If stereo rendering is enabled, update the size and offset appropriately for this pass
const bool bNeedStereo = (StereoPass != eSSP_FULL) && GEngine->IsStereoscopic3D();
if (bNeedStereo)
{
GEngine->StereoRenderingDevice->AdjustViewRect(StereoPass, X, Y, SizeX, SizeY);
}
// scale distances for cull distance purposes by the ratio of our current FOV to the default FOV
PlayerController->LocalPlayerCachedLODDistanceFactor = ViewInfo.FOV / FMath::Max<float>(0.01f, (PlayerController->PlayerCameraManager != NULL) ? PlayerController->PlayerCameraManager->DefaultFOV : 90.f);
FVector StereoViewLocation = ViewInfo.Location;
if (bNeedStereo || (GEngine->HMDDevice.IsValid() && GEngine->HMDDevice->IsHeadTrackingAllowed()))
{
GEngine->StereoRenderingDevice->CalculateStereoViewOffset(StereoPass, ViewInfo.Rotation, GetWorld()->GetWorldSettings()->WorldToMeters, StereoViewLocation);
}
// 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 (!bNeedStereo)
{
// Create the projection matrix (and possibly constrain the view rectangle)
FMinimalViewInfo::CalculateProjectionMatrixGivenView(ViewInfo, AspectRatioAxisConstraint, ViewportClient->Viewport, /*inout*/ ProjectionData);
}
else
{
// Let the stereoscopic rendering device handle creating its own projection matrix, as needed
ProjectionData.ProjectionMatrix = GEngine->StereoRenderingDevice->GetStereoProjectionMatrix(StereoPass, ViewInfo.FOV);
// calculate the out rect
ProjectionData.SetViewRectangle(FIntRect(X, Y, X + SizeX, Y + SizeY));
}
return true;
}
FMinimalViewInfo::CalculateProjectionMatrixGivenView(ViewInfo, AspectRatioAxisConstraint, ViewportClient->Viewport, /*inout*/ ProjectionData);
Editor 模式
Tick
FEditorViewportClient::Draw
FEditorViewportClient::CalcSceneView
FMinimalViewInfo::CalculateProjectionMatrixGivenView
void FEditorViewportClient::Draw(FViewport* InViewport, FCanvas* Canvas){
FSceneView* View = CalcSceneView( &ViewFamily );
}
FSceneView* FEditorViewportClient::CalcSceneView(FSceneViewFamily* ViewFamily)
{
FSceneViewInitOptions ViewInitOptions;
FViewportCameraTransform& ViewTransform = GetViewTransform();
const ELevelViewportType EffectiveViewportType = GetViewportType();
const FVector& ViewLocation = ViewTransform.GetLocation();
const FRotator& ViewRotation = ViewTransform.GetRotation();
const FIntPoint ViewportSizeXY = Viewport->GetSizeXY();
FIntRect ViewRect = FIntRect(0, 0, ViewportSizeXY.X, ViewportSizeXY.Y);
ViewInitOptions.SetViewRectangle(ViewRect);
// no matter how we are drawn (forced or otherwise), reset our time here
TimeForForceRedraw = 0.0;
const bool bConstrainAspectRatio = bUseControllingActorViewInfo && ControllingActorViewInfo.bConstrainAspectRatio;
const EAspectRatioAxisConstraint AspectRatioAxisConstraint = GetDefault<ULevelEditorViewportSettings>()->AspectRatioAxisConstraint;
//视图矩阵
ViewInitOptions.ViewOrigin = ViewLocation;
if (bUseControllingActorViewInfo)
{
ViewInitOptions.ViewRotationMatrix = FInverseRotationMatrix(ViewRotation) * FMatrix(
FPlane(0, 0, 1, 0),
FPlane(1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, 0, 1));
FMinimalViewInfo::CalculateProjectionMatrixGivenView(ControllingActorViewInfo, AspectRatioAxisConstraint, Viewport, /*inout*/ ViewInitOptions);
}
else
{
//
if (EffectiveViewportType == LVT_Perspective)
{
if (bUsingOrbitCamera)
{
ViewInitOptions.ViewRotationMatrix = FTranslationMatrix(ViewLocation) * ViewTransform.ComputeOrbitMatrix();
}
else
{
ViewInitOptions.ViewRotationMatrix = FInverseRotationMatrix(ViewRotation);
}
ViewInitOptions.ViewRotationMatrix = ViewInitOptions.ViewRotationMatrix * FMatrix(
FPlane(0, 0, 1, 0),
FPlane(1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, 0, 1));
float MinZ = GetNearClipPlane();
float MaxZ = MinZ;
// Avoid zero ViewFOV's which cause divide by zero's in projection matrix
float MatrixFOV = FMath::Max(0.001f, ViewFOV) * (float)PI / 360.0f;
if (bConstrainAspectRatio)
{
if ((int32)ERHIZBuffer::IsInverted != 0)
{
ViewInitOptions.ProjectionMatrix = FReversedZPerspectiveMatrix(
MatrixFOV,
MatrixFOV,
1.0f,
AspectRatio,
MinZ,
MaxZ
);
}
else
{
ViewInitOptions.ProjectionMatrix = FPerspectiveMatrix(
MatrixFOV,
MatrixFOV,
1.0f,
AspectRatio,
MinZ,
MaxZ
);
}
}
else
{
float XAxisMultiplier;
float YAxisMultiplier;
if (((ViewportSizeXY.X > ViewportSizeXY.Y) && (AspectRatioAxisConstraint == AspectRatio_MajorAxisFOV)) || (AspectRatioAxisConstraint == AspectRatio_MaintainXFOV))
{
//if the viewport is wider than it is tall
XAxisMultiplier = 1.0f;
YAxisMultiplier = ViewportSizeXY.X / (float)ViewportSizeXY.Y;
}
else
{
//if the viewport is taller than it is wide
XAxisMultiplier = ViewportSizeXY.Y / (float)ViewportSizeXY.X;
YAxisMultiplier = 1.0f;
}
if ((int32)ERHIZBuffer::IsInverted != 0)
{
ViewInitOptions.ProjectionMatrix = FReversedZPerspectiveMatrix(
MatrixFOV,
MatrixFOV,
XAxisMultiplier,
YAxisMultiplier,
MinZ,
MaxZ
);
}
else
{
ViewInitOptions.ProjectionMatrix = FPerspectiveMatrix(
MatrixFOV,
MatrixFOV,
XAxisMultiplier,
YAxisMultiplier,
MinZ,
MaxZ
);
}
}
}
else
{
static_assert((int32)ERHIZBuffer::IsInverted != 0, "Check all the Rotation Matrix transformations!");
float ZScale = 0.5f / HALF_WORLD_MAX;
float ZOffset = HALF_WORLD_MAX;
//The divisor for the matrix needs to match the translation code.
const float Zoom = GetOrthoUnitsPerPixel(Viewport);
float OrthoWidth = Zoom * ViewportSizeXY.X / 2.0f;
float OrthoHeight = Zoom * ViewportSizeXY.Y / 2.0f;
if (EffectiveViewportType == LVT_OrthoXY)
{
ViewInitOptions.ViewRotationMatrix = FMatrix(
FPlane(1, 0, 0, 0),
FPlane(0, -1, 0, 0),
FPlane(0, 0, -1, 0),
FPlane(0, 0, -ViewLocation.Z, 1));
}
else if (EffectiveViewportType == LVT_OrthoXZ)
{
ViewInitOptions.ViewRotationMatrix = FMatrix(
FPlane(1, 0, 0, 0),
FPlane(0, 0, -1, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, -ViewLocation.Y, 1));
}
else if (EffectiveViewportType == LVT_OrthoYZ)
{
ViewInitOptions.ViewRotationMatrix = FMatrix(
FPlane(0, 0, 1, 0),
FPlane(1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, ViewLocation.X, 1));
}
else if (EffectiveViewportType == LVT_OrthoNegativeXY)
{
ViewInitOptions.ViewRotationMatrix = FMatrix(
FPlane(-1, 0, 0, 0),
FPlane(0, -1, 0, 0),
FPlane(0, 0, 1, 0),
FPlane(0, 0, -ViewLocation.Z, 1));
}
else if (EffectiveViewportType == LVT_OrthoNegativeXZ)
{
ViewInitOptions.ViewRotationMatrix = FMatrix(
FPlane(-1, 0, 0, 0),
FPlane(0, 0, 1, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, -ViewLocation.Y, 1));
}
else if (EffectiveViewportType == LVT_OrthoNegativeYZ)
{
ViewInitOptions.ViewRotationMatrix = FMatrix(
FPlane(0, 0, -1, 0),
FPlane(-1, 0, 0, 0),
FPlane(0, 1, 0, 0),
FPlane(0, 0, ViewLocation.X, 1));
}
else
{
// Unknown viewport type
check(false);
}
ViewInitOptions.ProjectionMatrix = FReversedZOrthoMatrix(
OrthoWidth,
OrthoHeight,
ZScale,
ZOffset
);
}
if (bConstrainAspectRatio)
{
ViewInitOptions.SetConstrainedViewRectangle(Viewport->CalculateViewExtents(AspectRatio, ViewRect));
}
}
ViewInitOptions.ViewFamily = ViewFamily;
ViewInitOptions.SceneViewStateInterface = ViewState.GetReference();
ViewInitOptions.ViewElementDrawer = this;
ViewInitOptions.BackgroundColor = GetBackgroundColor();
ViewInitOptions.EditorViewBitflag = (uint64)1 << ViewIndex, // send the bit for this view - each actor will check it's visibility bits against this
// for ortho views to steal perspective view origin
ViewInitOptions.OverrideLODViewOrigin = FVector::ZeroVector;
ViewInitOptions.bUseFauxOrthoViewPos = true;
if (bUseControllingActorViewInfo)
{
ViewInitOptions.bUseFieldOfViewForLOD = ControllingActorViewInfo.bUseFieldOfViewForLOD;
}
ViewInitOptions.OverrideFarClippingPlaneDistance = FarPlane;
ViewInitOptions.CursorPos = CurrentMousePos;
FSceneView* View = new FSceneView(ViewInitOptions);
View->SubduedSelectionOutlineColor = GEngine->GetSubduedSelectionOutlineColor();
ViewFamily->Views.Add(View);
View->StartFinalPostprocessSettings(ViewLocation);
OverridePostProcessSettings( *View );
View->EndFinalPostprocessSettings(ViewInitOptions);
return View;
}
以上最主要的还是这个函数:
FMinimalViewInfo::CalculateProjectionMatrixGivenView(ControllingActorViewInfo, AspectRatioAxisConstraint, Viewport, /*inout*/ ViewInitOptions);
void FMinimalViewInfo::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)
{
// Enforce a particular aspect ratio for the render of the scene.
// Results in black bars at top/bottom etc.
InOutProjectionData.SetConstrainedViewRectangle(Viewport->CalculateViewExtents(ViewInfo.AspectRatio, InOutProjectionData.GetViewRect()));
//
if (ViewInfo.ProjectionMode == ECameraProjectionMode::Orthographic)
{
const float YScale = 1.0f / ViewInfo.AspectRatio;
const float OrthoWidth = ViewInfo.OrthoWidth / 2.0f;
const float OrthoHeight = ViewInfo.OrthoWidth / 2.0f * YScale;
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
{
// Avoid divide by zero in the projection matrix calculation by clamping FOV
InOutProjectionData.ProjectionMatrix = FReversedZPerspectiveMatrix(
FMath::Max(0.001f, ViewInfo.FOV) * (float)PI / 360.0f,
ViewInfo.AspectRatio,
1.0f,
GNearClippingPlane );
}
}
else
{
// Avoid divide by zero in the projection matrix calculation by clamping FOV
float MatrixFOV = FMath::Max(0.001f, ViewInfo.FOV) * (float)PI / 360.0f;
float XAxisMultiplier;
float YAxisMultiplier;
const FIntRect& ViewRect = InOutProjectionData.GetViewRect();
const int32 SizeX = ViewRect.Width();
const int32 SizeY = ViewRect.Height();
// if x is bigger, and we're respecting x or major axis, AND mobile isn't forcing us to be Y axis aligned
if (((SizeX > SizeY) && (AspectRatioAxisConstraint == AspectRatio_MajorAxisFOV)) || (AspectRatioAxisConstraint == AspectRatio_MaintainXFOV) || (ViewInfo.ProjectionMode == ECameraProjectionMode::Orthographic))
{
//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;
}
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
{
InOutProjectionData.ProjectionMatrix = FReversedZPerspectiveMatrix(
MatrixFOV,
MatrixFOV,
XAxisMultiplier,
YAxisMultiplier,
GNearClippingPlane,
GNearClippingPlane
);
}
}
}
Game模式下自定义透视投影矩阵
UE4 的投影矩阵比较特殊,近剪裁和远剪裁同为一个值:
FReversedZPerspectiveMatrix(
MatrixFOV,
MatrixFOV,
XAxisMultiplier,
YAxisMultiplier,
GNearClippingPlane,
GNearClippingPlane
);
如果想自定义投影矩阵,具体请参考链接,通过继承ULocalPlayer
重写CalcSceneView
函数。
FSceneView * UOffAxisLocalPlayer::CalcSceneView(FSceneViewFamily * ViewFamily, FVector &amp;OutViewLocation, FRotator &amp;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->(ProjectionMatrix);
}
return View;
}