読者です 読者をやめる 読者になる 読者になる

ムラサメ研究ブログ

主にゲームやプログラミングのログ

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を直接触る上級者は、上記関数をさんこうにして 独自で関数を作れ!

以上