Unreal 4.13でWorldViewProjectionを探す旅
調査中内容でも書かないよりはましかとおもって
自分用のメモついでに。
UnrealEngineは基本的にMatrixを扱わずに開発出来るように作られているように思う BluePrintでもほとんどMatrix関係の関数がないし
その代わりに、RotaterやTranslator等を作って Applyする World変換もViewPort変換も専用の関数があり Matrixを使う必要性がない
そのあたりの調査をする
WorldViewProjection関連
APlayerControllerに色々と入っている PlayerControllerは色々な機能があり レベル(マップ)遷移、Spectator(観戦モード)、FOV、2D 3D座標変換、マウス座標のオブジェクト取得、(ボイス)チャット機能 カメラアニメ(手ブレ表現等)、音声再生、フォースフィードバック、キー入力・・・ 書ききれない機能がある
今回は 座標変換関連を調査
Projection(3D->2D)
いきなり主目的。 ProjectWorldLocationToScreen ProjectWorldLocationToScreenWithDistance の2関数がそれにあたる
PlayerController.cpp
bool APlayerController::ProjectWorldLocationToScreen(FVector WorldLocation, FVector2D& ScreenLocation) const { return UGameplayStatics::ProjectWorldToScreen(this, WorldLocation, ScreenLocation); } bool APlayerController::ProjectWorldLocationToScreenWithDistance(FVector WorldLocation, FVector& ScreenLocation) const { FVector2D ScreenLoc2D; if (UGameplayStatics::ProjectWorldToScreen(this, WorldLocation, ScreenLoc2D)) { // find distance ULocalPlayer const* const LP = GetLocalPlayer(); if (LP && LP->ViewportClient) { // get the projection data FSceneViewProjectionData ProjectionData; if (LP->GetProjectionData(LP->ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData)) { ScreenLocation = FVector(ScreenLoc2D.X, ScreenLoc2D.Y, FVector::Dist(ProjectionData.ViewOrigin, WorldLocation)); return true; } } } return false; }
GameplayStatics.cpp
bool UGameplayStatics::ProjectWorldToScreen(APlayerController const* Player, const FVector& WorldPosition, FVector2D& ScreenPosition) { 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 ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix(); return FSceneView::ProjectWorldToScreen(WorldPosition, ProjectionData.GetConstrainedViewRect(), ViewProjectionMatrix, ScreenPosition); } } ScreenPosition = FVector2D::ZeroVector; return false; }
ScreenView.cpp
bool FSceneView::ProjectWorldToScreen(const FVector& WorldPosition, const FIntRect& ViewRect, const FMatrix& ViewProjectionMatrix, FVector2D& out_ScreenPos) { FPlane Result = ViewProjectionMatrix.TransformFVector4(FVector4(WorldPosition, 1.f)); if ( Result.W > 0.0f ) { // the result of this will be x and y coords in -1..1 projection space const float RHW = 1.0f / Result.W; FPlane PosInScreenSpace = FPlane(Result.X * RHW, Result.Y * RHW, Result.Z * RHW, Result.W); // Move from projection space to normalized 0..1 UI space const float NormalizedX = ( PosInScreenSpace.X / 2.f ) + 0.5f; const float NormalizedY = 1.f - ( PosInScreenSpace.Y / 2.f ) - 0.5f; FVector2D RayStartViewRectSpace( (float)ViewRect.Min.X + ( NormalizedX * (float)ViewRect.Width() ), (float)ViewRect.Min.Y + ( NormalizedY * (float)ViewRect.Height() ) ); out_ScreenPos = RayStartViewRectSpace; return true; } return false; }
細かい部分はおいておいて
ProjectWorldLocationToScreen はViewPort変換までおこない3D座標からスクリーン座標に変換 ProjectWorldLocationToScreenWithDistance は、上記に加えZにターゲットまでの距離が入る ようだ
残念ながら ViewPort変換の入らない、WorldViewProjectionは直接取得出来ない雰囲気
GetProjectionDataでProjectionを取得し、TransformFVector4で Vectorに適用している。 この時の座標系は同次座標ではないので wで除算すれば、-1..1 のWVP座標になるようだ そこに Screenサイズを適用し最終的なScreen座標を取得している
ただし W<0 の時は計算されないので、プレイヤーの背後は計算されない。 今回はプレイヤーの背後も必要だったので、これらの関数を W<0でも計算できるように改良したり -1..1 の座標系が欲しかったので
上記関数を自作した
DeprojectMousePositionToWorld
PlayerController.cpp
bool APlayerController::DeprojectMousePositionToWorld(FVector& WorldLocation, FVector& WorldDirection) const { ULocalPlayer* const LocalPlayer = GetLocalPlayer(); if (LocalPlayer && LocalPlayer->ViewportClient) { FVector2D ScreenPosition; if (LocalPlayer->ViewportClient->GetMousePosition(ScreenPosition)) { return UGameplayStatics::DeprojectScreenToWorld(this, ScreenPosition, WorldLocation, WorldDirection); } } return false; }
GameplayStatics.cpp
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; }
GameplayStatics.cpp
void FSceneView::DeprojectScreenToWorld(const FVector2D& ScreenPos, const FIntRect& ViewRect, const FMatrix& InvViewProjMatrix, FVector& out_WorldOrigin, FVector& out_WorldDirection) { float PixelX = FMath::TruncToFloat(ScreenPos.X); float PixelY = FMath::TruncToFloat(ScreenPos.Y); // Get the eye position and direction of the mouse cursor in two stages (inverse transform projection, then inverse transform view). // This avoids the numerical instability that occurs when a view matrix with large translation is composed with a projection matrix // Get the pixel coordinates into 0..1 normalized coordinates within the constrained view rectangle const float NormalizedX = (PixelX - ViewRect.Min.X) / ((float)ViewRect.Width()); const float NormalizedY = (PixelY - ViewRect.Min.Y) / ((float)ViewRect.Height()); // Get the pixel coordinates into -1..1 projection space const float ScreenSpaceX = (NormalizedX - 0.5f) * 2.0f; const float ScreenSpaceY = ((1.0f - NormalizedY) - 0.5f) * 2.0f; // The start of the raytrace is defined to be at mousex,mousey,1 in projection space (z=1 is near, z=0 is far - this gives us better precision) // To get the direction of the raytrace we need to use any z between the near and the far plane, so let's use (mousex, mousey, 0.5) const FVector4 RayStartProjectionSpace = FVector4(ScreenSpaceX, ScreenSpaceY, 1.0f, 1.0f); const FVector4 RayEndProjectionSpace = FVector4(ScreenSpaceX, ScreenSpaceY, 0.5f, 1.0f); // Projection (changing the W coordinate) is not handled by the FMatrix transforms that work with vectors, so multiplications // by the projection matrix should use homogeneous coordinates (i.e. FPlane). 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); // divide vectors by W to undo any projection and get the 3-space coordinate if (HGRayStartWorldSpace.W != 0.0f) { RayStartWorldSpace /= HGRayStartWorldSpace.W; } if (HGRayEndWorldSpace.W != 0.0f) { RayEndWorldSpace /= HGRayEndWorldSpace.W; } const FVector RayDirWorldSpace = (RayEndWorldSpace - RayStartWorldSpace).GetSafeNormal(); // Finally, store the results in the outputs out_WorldOrigin = RayStartWorldSpace; out_WorldDirection = RayDirWorldSpace; }
スクリーン座標からワールド座標に変換。 やってることは単純で、先程の WVPVpの逆行列を掛けているだけ ただ 2D座標から3D座標への変換は距離により無限に座標があるので Locationと方向をかえす。 要はRayを返すから Distanceを計算してポジションは取ってね って事
オブジェクトとの当たり判定 (2D->3D)
GetHitResultUnderCursor GetHitResultUnderCursorByChannel GetHitResultUnderCursorForObjects GetHitResultUnderFinger GetHitResultUnderFingerByChannel GetHitResultUnderFingerForObjects GetHitResultAtScreenPosition GetHitResultAtScreenPosition
マウスクリック、タッチ 等でスクリーン座標をとり 上記のようにRayを作り オブジェクトと衝突したらその3D座標を返す
ボタンやオブジェクトのクリックに使える
チャンネル指定や、オブジェクトタイプで衝突を決めれる このあたりは RayTraceと同じ仕様
まとめ
APlayerController には、プロジェクション、デプロジェクションがあり、ワールド座標とスクリーン座標の変換は容易である ただし 後ろが取れなかったり、ViewPort座標でしか取れなかったりと 多少不便だが 普通の人は直接Matrixを使わないので問題がない
Matrixを直接触る上級者は、上記関数をさんこうにして 独自で関数を作れ!
以上