ゆいブロ

自分の好きなことや知見をまとめていくブログです。

スポンサーリンク

Unityのカメラを使った2次元↔3次元座標変換を自前で実装する

はじめに

UnityのScreenToWorldPointやWorldToScreenPointは、Cameraのオブジェクトに座標などを反映させたうえでメソッドを呼ばないと値が返ってきません。 Cameraにいちいち値を代入せずに自前で計算したい場面が出てきたので、自前で実装してきました。

Github

 今回実装したソースコードは下記にあげてあります

https://github.com/yui-tech/UnityCameraUtility/blob/main/UnityCameraUtility.cs:embed:cite?slice=7:10"

各行列の実装

自前計算するにあたって、ビュー行列、プロジェクション行列、ビューポート行列が必要になるため、下記に記載します。 尚、Cameraコンポーネントを必要としないライブラリのため、UnityEngine.Matrix4x4のみ使用しておりますので、他言語で実装する場合は各言語のMatrix系のライブラリなどで置き換えて実装して頂ければと思います。 また、UnityはOpelGL系での座標計算が行われるため、DirectX系で実装する場合は行列計算の部分を置き換える必要があります。

Matrix4x4を使用している部分についての解説

・Matrix4x4.TRS(Vector3 pos, Quaternion q, Vector3 s)  平行移動、回転、スケールの入力を使用したアフィン変換行列を作成します。

・Matrix4x4.Perspective(float fov, float aspect, float zNear, float zFar)  透視投影行列を作成します。

ビュー行列

 ビュー行列は、カメラの視点を決めるための行列で、カメラの座標と方向を入力としたアフィン変換の逆行列であらわされます。今回はわざとスケール値のZに-1fを入れていますが、 これは、Unityの行列計算はOpenGLベースで行われるのですが、Unityのカメラの前方方向がOpenGLと異なる(負のZ軸ではなく正のZ軸である)ため、Z軸に-1fをかけることでOpenGLベースの計算に合わせています。

プロジェクション行列(射影変換行列)

 カメラ座標系からクリップ座標系への変換行列を作成します。内部処理はUnityEngine.Matrix4x4.Perspectiveに丸投げしています。

/// <summary>
/// プロジェクション行列の作成
/// </summary>
///
/// <param name="fov"> Field Of View </param>
/// <param name="width"> 画面の横幅 </param>
/// <param name="height"> 画面の縦幅 </param>
/// <param name="nearClipPlane"> ニアクリッププレーン </param>
/// <param name="farClipPlane"> ファークリッププレーン </param>
public static Matrix4x4 ProjectionMatrix(float fov, float width, float height, float nearClipPlane, float farClipPlane)
{
    return Matrix4x4.Perspective(fov, width / height, nearClipPlane, farClipPlane);
}

ビューポート行列

 スクリーン座標系に変換する行列を作成します。

/// <summary>
/// ビューポート行列の作成
/// </summary>
///
/// <param name="width"> 画面の横幅 </param>
/// <param name="height"> 画面の縦幅 </param>
/// <param name="nearClipPlane"> ニアクリッププレーン </param>
/// <param name="farClipPlane"> ファークリッププレーン </param>
public static Matrix4x4 ViewportMatrix(float width, float height, float nearClipPlane, float farClipPlane)
{
    Matrix4x4 mat = Matrix4x4.identity;
    mat.m00 = width * 0.5f;
    mat.m03 = width * 0.5f;
    mat.m11 = height * 0.5f;
    mat.m13 = height * 0.5f;
    mat.m22 = (farClipPlane - nearClipPlane) * 0.5f;
    mat.m23 = (farClipPlane + nearClipPlane) * 0.5f;
    return mat;
}

ScreenToWorldPoint

 2D座標から3D座標への変換です。  ビュー・プロジェクション・ビューポート行列の逆行列を作成し、その順番にかけたマトリクスを作成します。  その後スクリーン座標とニアクリップから算出した座標とマトリクスで計算を行い、算出した値をwで割ることで3D座標が算出されます。  引数のdistanceにはカメラからの距離を入れることで、指定の距離での3D座標を求められるようになっています。

/// <summary>
/// Screen座標からWorld座標への変換
/// </summary>
///
/// <param name="screenPosition"> Screen座標 </param>
/// <param name="nearClipPlane"> ニアクリッププレーン </param>
/// <param name="farClipPlane"> ファークリッププレーン </param>
/// <param name="position"> Cameraの座標 </param>
/// <param name="rotation"> Cameraの回転 </param>
/// <param name="fov"> Field Of View </param>
/// <param name="width"> 画面の横幅 </param>
/// <param name="height"> 画面の縦幅 </param>
/// <param name="distance"> カメラからの距離 </param>
///
/// <returns> 変換されたWorld座標 </returns>
public static Vector3 ScreenToWorldPoint(Vector2 screenPosition, float nearClipPlane, float farClipPlane, Vector3 position, Quaternion rotation, float fov, float width, float height, float distance)
{
    var customNearClipPlane = nearClipPlane + distance;
    Matrix4x4 viewMatrixInverse = WorldToCameraMatrix(position, rotation).inverse;
    Matrix4x4 projectionMatrixInverse = ProjectionMatrix(fov, width, height, customNearClipPlane, farClipPlane).inverse;
    Matrix4x4 viewportMatrixInverse = ViewportMatrix(width, height, customNearClipPlane, farClipPlane).inverse;

    Matrix4x4 matrix = viewMatrixInverse * projectionMatrixInverse * viewportMatrixInverse;

    Vector3 worldPosition = new Vector3(screenPosition.x, screenPosition.y, customNearClipPlane);

    float x = worldPosition.x * matrix.m00 + worldPosition.y * matrix.m01 + worldPosition.z * matrix.m02 + matrix.m03;
    float y = worldPosition.x * matrix.m10 + worldPosition.y * matrix.m11 + worldPosition.z * matrix.m12 + matrix.m13;
    float z = worldPosition.x * matrix.m20 + worldPosition.y * matrix.m21 + worldPosition.z * matrix.m22 + matrix.m23;
    float w = worldPosition.x * matrix.m30 + worldPosition.y * matrix.m31 + worldPosition.z * matrix.m32 + matrix.m33;

    if (w == 0f)
    {
        return Vector3.zero;
    }

    x /= w;
    y /= w;
    z /= w;

    return new Vector3(x, y, z);
}

WorldToScreenPoint

 3D座標から2D座標への変換です。  プロジェクション行列とビュー行列をかけたマトリクスを作成し、3D座標をかけることでクリッピング座標を作成します。その後、wで割った後にスクリーン座標への変換処理を行い算出します。

/// <summary>
/// World座標からScreen座標への変換
/// </summary>
///
/// <url> https://stackoverflow.com/questions/8491247/c-opengl-convert-world-coords-to-screen2d-coords </url>
///
/// <param name="worldPosition"> World座標 </param>
/// <param name="nearClipPlane"> ニアクリッププレーン </param>
/// <param name="farClipPlane"> ファークリッププレーン </param>
/// <param name="position"> Cameraの座標 </param>
/// <param name="rotation"> Cameraの回転 </param>
/// <param name="fov"> Field Of View </param>
/// <param name="width"> 画面の横幅 </param>
/// <param name="height"> 画面の縦幅 </param>
///
/// <returns> 変換されたWorld座標 </returns>
public static Vector2 WorldToScreenPoint(Vector3 worldPosition, float nearClipPlane, float farClipPlane, Vector3 position, Quaternion rotation, float fov, float width, float height)
{
    Matrix4x4 viewMatrix = WorldToCameraMatrix(position, rotation);
    Matrix4x4 projectionMatrix = ProjectionMatrix(fov, width, height, camera.nearClipPlane, camera.farClipPlane);

    Matrix4x4 matrix = projectionMatrix * viewMatrix;

    Vector4 clipSpacePos = matrix * new Vector4(worldPosition.x, worldPosition.y, worldPosition.z, 1f);

    Vector3 ndcSpacePos = clipSpacePos;

    if (clipSpacePos.w == 0f)
    {
        return Vector2.zero;
    }

    ndcSpacePos.x /= clipSpacePos.w;
    ndcSpacePos.y /= clipSpacePos.w;
    ndcSpacePos.z /= clipSpacePos.w;

    Vector2 windowSpacePos = ndcSpacePos;
    windowSpacePos.x = ((ndcSpacePos.x + 1.0f) * 0.5f) * width;
    windowSpacePos.y = ((ndcSpacePos.y + 1.0f) * 0.5f) * height;
    return windowSpacePos;
}

おわりに

 今回はWorldToScreenPointと、ScreenToWorldPointをUnityのCameraを使用せずに計算する関数についてまとめました。普段はあまり使用することはないと思いますが、必要になった場合は参考にしていただければと思います。

参考文献

forum.unity.com

stackoverflow.com

edom18.hateblo.jp