哈喽,大家好,一起分享一下关于UE5如何将屏幕坐标转为世界坐标的方法。
屏幕坐标转为世界坐标
在PlayerControoler下找到这个函数: DeprojectScreenPositionToWorld
bool APlayerController::DeprojectScreenPositionToWorld(float ScreenX, float ScreenY, FVector& WorldLocation, FVector& WorldDirection) const;
只要将屏幕位置传进去就可以获取的基于屏幕位置的世界位置和方向。
那它是怎么实现的呢?
bool UGameplayStatics::DeprojectScreenToWorld(APlayerController const* Player, const FVector2D& ScreenPosition, FVector& WorldPosition, FVector& WorldDirection)
{
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 InvViewProjMatrix = ProjectionData.ComputeViewProjectionMatrix().InverseFast();
FSceneView::DeprojectScreenToWorld(ScreenPosition, ProjectionData.GetConstrainedViewRect(), InvViewProjMatrix, /*out*/ WorldPosition, /*out*/ WorldDirection);
return true;
}
}
// something went wrong, zero things and return false
WorldPosition = FVector::ZeroVector;
WorldDirection = FVector::ZeroVector;
return false;
}
其中的FSceneViewProjectionData是一个关于场景视口的数据描述
// Projection data for a FSceneView
struct FSceneViewProjectionData
{
/** The view origin. */
FVector ViewOrigin;
/** Rotation matrix transforming from world space to view space. */
FMatrix ViewRotationMatrix;
/** UE4 projection matrix projects such that clip space Z=1 is the near plane, and Z=0 is the infinite far plane. */
FMatrix ProjectionMatrix;
protected:
//The unconstrained (no aspect ratio bars applied) view rectangle (also unscaled)
FIntRect ViewRect;
// The constrained view rectangle (identical to UnconstrainedUnscaledViewRect if aspect ratio is not constrained)
FIntRect ConstrainedViewRect;
};
ViewOrigin:是视口的原点
ViewRotationMatrix:是一个旋转矩阵 可以理解为ViewMatrix 只是不包含平移矩阵
ProjectionMatrix:投影矩阵
需要区分的是 ViewMatrix 是把世界点的物体转为视口下的局部坐标,而WorldMatrix是把局部坐标转为世界的坐标。
其中对 VP 进行求逆 希望可以将视口下的点通过这个求逆来转为世界坐标
FMatrix ComputeViewProjectionMatrix() const
{
return FTranslationMatrix(-ViewOrigin) * ViewRotationMatrix * ProjectionMatrix;
}
FMatrix const InvViewProjMatrix = ProjectionData.ComputeViewProjectionMatrix().InverseFast();
当获取了 FSceneViewProjectionData 结构的数据,接下来我们看看 DeprojectScreenToWorld
我们知道射线的公式是
我们现在来看看 DeprojectScreenToWorld 这个函数 里面都干了什么。
我们知道如果将一个局部的模型点从局部空间到屏幕空间,它经历了这些变化
a.通过worldmatrix把模型的局部点转为世界点
b.通过viewmatrix矩阵把点从世界位置转为视口下
c.通过projematrix投影矩阵把视口点转为近剪裁面上的点
d.通过投射除法映射到NDC空间[-1.1]
e.通过半兰伯特手法把[-1.1]映射到[0,1]
f.再和屏幕宽高相乘获得屏幕的位置
如果要实现拾取,那么它将是一个逆的过程,已经知道屏幕位置,怎么求这个屏幕位置对应的世界位置和方向?
这里复习一个矩阵的知识。
如果一个worldmatrix可以把点从局部空间转为世界空间,那么想把这个点从世界空间转为局部空间,只需要对这个worldmatrix求逆就可以了,可以理解逆运算是矩阵的除法。
比如 已知 ,现在希望通过500求 100,只需要 就可以得到100。其中 就是5的逆,矩阵也是一样的。
我们来看看UE5是怎么做的:
1.先拿到屏幕坐标
float PixelX = FMath::TruncToFloat(ScreenPos.X);
float PixelY = FMath::TruncToFloat(ScreenPos.Y);
2.拿到坐标后把坐标映射到0-1范围
const float NormalizedX = (PixelX - ViewRect.Min.X) / ((float)ViewRect.Width());
const float NormalizedY = (PixelY - ViewRect.Min.Y) / ((float)ViewRect.Height());
3.将坐标映射到NDC空间[-1,1]
const float ScreenSpaceX = (NormalizedX - 0.5f) * 2.0f;
const float ScreenSpaceY = ((1.0f - NormalizedY) - 0.5f) * 2.0f;
4.射线在投影空间的开始和结尾,其中0.01是近剪裁面,1是远剪裁面
const FVector4 RayStartProjectionSpace = FVector4(ScreenSpaceX, ScreenSpaceY, 1.0f, 1.0f);
const FVector4 RayEndProjectionSpace = FVector4(ScreenSpaceX, ScreenSpaceY, 0.01f, 1.0f);
5.通过前面的VP的逆运算 将裁剪空间的点转为世界空间
const FVector4 HGRayStartWorldSpace = InvViewProjMatrix.TransformFVector4(RayStartProjectionSpace);
const FVector4 HGRayEndWorldSpace = InvViewProjMatrix.TransformFVector4(RayEndProjectionSpace);
FVector RayStartWorldSpace(HGRayStartWorldSpace.X, HGRayStartWorldSpace.Y, HGRayStartWorldSpace.Z);
FVector RayEndWorldSpace(HGRayEndWorldSpace.X, HGRayEndWorldSpace.Y, HGRayEndWorldSpace.Z);
6.将向量除以W以撤消任何投影并获得3-space坐标(可以理解归一到世界的近剪裁面)
if (HGRayStartWorldSpace.W != 0.0f)
{
RayStartWorldSpace /= HGRayStartWorldSpace.W;
}
if (HGRayEndWorldSpace.W != 0.0f)
{
RayEndWorldSpace /= HGRayEndWorldSpace.W;
}
7.拿到方向
const FVector RayDirWorldSpace = (RayEndWorldSpace - RayStartWorldSpace).GetSafeNormal();
8.最终拿到需要的值
out_WorldOrigin = RayStartWorldSpace;
out_WorldDirection = RayDirWorldSpace;
有机会我们再讨论一下射线
那我们现在将这个的思路运用在 自研小引擎中: