ゆいブロ

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

スポンサーリンク

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

【Unity】Timelineに機能を追加できるDefault Playablesの紹介

  • はじめに
  • Default Playablesの追加機能
    • Light Control Track
    • Nav Mesh Agent
    • Screen Fader Track
    • Text Switcher Track
    • Time Dialation Track
    • Transform Tween Track
    • Video Script Playable Track
  • Timeline Playable Wizardの説明

はじめに

Timelineは、既に用意されている機能では足りないことがあると思いますが、 自分でコードを書くことで自作機能を追加することができます。

ただ、機能を追加するには複数のScriptファイルを作成しなければならず、初心者が一から実装するには少しハードルが高いところがあります。

そこで、Unity公式が提供しているDefault PlayablesというAssetをAsset Storeからダウンロードすることで、実装が少し簡単になります。

Default Playablesの追加機能

Default Playablesをプロジェクトにインストールすると、Timelineにいくつかの機能が追加されます。

f:id:yui-frapper:20200206001832p:plain

ただしものによっては使い勝手が悪く、自作した方がいいものもあります…。

続きを読む

【Git】最近ハマっているGitクライアント「Fork」のインストール方法とおすすめポイントを紹介する

  • はじめに
  • Forkのインストール方法と使用方法
    • リポジトリのクローン方法
    • メニューアイコンの説明
  • Forkのおすすめポイント
    • Quick Launch
    • Conflict解消
    • 差分の破棄
    • 動作が軽快
    • 無料で使える
  • 最後に

はじめに

SourceTreeやGithub Desktop、Tortoise Gitなど様々なGitクライアントがありますが、その中でも最近自分がハマっている「Fork」というGitクライアントを紹介します。

Forkのインストール方法と使用方法

↓こちらから公式サイトにアクセスし、使用するPCのOSに合ったインストーラーをダウンロードします。

git-fork.com

ダウンロードが完了したら、インストールするだけで使用できます。

起動すると、このようなスタイリッシュな画面が表示されます。

f:id:yui-frapper:20200206232227p:plain

続きを読む

【Unity】TimelineのTrackを別のTimelineなどにコピーする際に、VirtualCameraの参照を上手く付け替える方法

  • はじめに
  • VirtualCameraはどこに参照がある?
  • 解決方法

はじめに

https://docs.unity3d.com/ja/2018.4/uploads/Main/timeline_splash.jpg

前回、TimelineのTrackを別のTimelineにコピーする方法を書きました。

www.yui-tech-blog.com

この記事の最後の方にCinemachineTrack内のClipで、VirtualCameraのBindが上手く引き継げない問題をあげていましたので 今回はこれを解決していこうと思います。

VirtualCameraはどこに参照がある?

まず、VirtualCameraのBindは、PlayableDirectorのBindingsに情報があります。 むしろ、TimelineClipやCinemachineShot側には、VirtualCameraを参照しているように見せかけて、実は参照自体は持っていません。

PlayableDiretorを経由しないと参照が取得できないようになっています。

実際のEditorの画面を用いて説明していきます。

続きを読む

【Unity】TimelineのTrackを別のTimelineにコピーする

  • はじめに
  • 実装の注意点
  • 実装方法
  • 解説
    • Reflectionで内部APIにアクセスする
    • Duplicateメソッドを呼ぶ
    • Bindのコピー

はじめに

https://docs.unity3d.com/ja/2018.4/uploads/Main/timeline_splash.jpg

TimelineAsset内のTrackを、別のTimelineにコピーしなければいけない時があり、
その際に他のTimelineにデータをコピーする方法を調べたのでまとめます。

この方法だと、TimelineのBind設定も引き継いでくれます(例外あり)。 引き継げないものに関しては後述します。

実装の注意点

Unity2019.2.17f1にて実装しております。

System.Reflectionを使用しているため、Unity2018以前などではネームスペースが変更されておりMethodInfo等が取得できない可能性があります。

実装方法

続きを読む

【Unity】TimelineをScriptから扱うときのTips

  • はじめに
  • トラック名で検索したトラックにオブジェクトをバインドさせる
  • 特定の型をバインドタイプとしてもつトラックにオブジェクトをバインドさせる
  • 指定トラックのクリップを取得する
  • 指定トラックのクリップを全取得する
  • Virtual Cameraの取得

はじめに

TimelineをScript制御する上で必要となるメソッドを紹介します。

なお、コード中に出てくるGetPlayableDirector()は、PlayableDirectorを取得するメソッドですので各自実装するか置き換えてください。

トラック名で検索したトラックにオブジェクトをバインドさせる

/// <summary>
/// オブジェクトをタイムラインのトラックにバインドさせる
/// </summary>
///
/// <param name="trackName"> トラック名 </param>
/// <param name="sourceComponent"> 紐づけたいオブジェクト </param>
public void BindTrack( string trackName, UnityEngine.Object sourceObject)
{
    foreach( PlayableBinding bind in GetPlayableDirector().playableAsset.outputs )
    {
        if( bind.streamName == trackName )
        {
            GetPlayableDirector().SetGenericBinding( bind.sourceObject, sourceObject );
            break;
        }
    }
}

特定の型をバインドタイプとしてもつトラックにオブジェクトをバインドさせる

/// <summary>
/// T型のバインドタイプを持ったトラックにT型のオブジェクトの参照を渡す
/// </summary>
///
/// <param name="bindObject"> バインドするオブジェクト </param>
public void BindObject<T>( T bindObject ) where T : UnityEngine.Object
{
    foreach( TrackAsset trackAsset in ( GetPlayableDirector().playableAsset as TimelineAsset ).GetOutputTracks() )
    {
        foreach( PlayableBinding binding in trackAsset.outputs )
        {
            if( binding.outputTargetType == typeof( T ) )
            {
                GetPlayableDirector().SetGenericBinding( binding.sourceObject, bindObject );
            }
        }
    }
}
続きを読む

【Unity】UnityでSpriteAtlasの差分が出る問題が解決した

はじめに

11月末くらいから、Unityで共同開発している際に、変更していないSpriteAtlasの差分が出続けてしまうという問題が出ました。

Unityの特定のバージョン以降で起きており、Unity2019だけでなく2020,2018でも起きており、自分も以前に記事を書いておりました。

www.yui-tech-blog.com

今回、この問題が修正されたので記事にしようと思います。

修正されたバージョンについて

下記のバージョンで修正されるみたいです。

  • 2020.1 => 2020.1.0a19
  • 2019.3 => 2019.3.0f5
  • 2019.2 => 2019.2.19f1
  • 2018.4 => 2018.4.16f1

実際に自分もUnity2019.2.19f1を試したところ、差分が出なくなりました。 というかstoredHash、という項目自体が削除されていました。

そもそもこの現象が起きてから、変更を無視してプレイしたりビルドしても何も問題が起きていなかったので、storedHashという項目自体が不要だったのかもしれません。修正に時間がかかったのは、この項目を内部で参照しているところを片っ端から切り離すのに工数がかかったのかなと思っています。

現在、Unity2018.4.16f1以外はリリースされているので、この問題が発生している方はアップデートしてみることをおすすめします。マイナーアップデートなので基本的にはアップグレードによる不具合はないと思いますので。

最後に

Unity2018はLTS(Long Term Support)になっているのに、いまだに修正バージョンがリリースされていませんが、LTSは優先的に修正されるという勝手なイメージがあったのでちょっと残念ですね…。 確かに現バージョンやアルファ版を積極的に開発しているためそちらを優先的にする気持ちはわかりますが、安定性を求めてLTSにしたのに後回しにされるなら、あまり意味ないじゃんと個人的には思ってしまいました。 2018を使用している方はアップデートが来るまで待つか、参考リンクのフォーラム上で紹介されている暫定対応策を試してみてください。

参考リンク

https://forum.unity.com/threads/spriteatlases-keep-changing-their-hash-without-sprites-changing-polluting-source-control.754739/

【Unity】新規限定Asset Storeの10%オフクーポンコードと、おすすめAssetの紹介!

  • 新規限定クーポンコード
  • おすすめアセット紹介
    • Playmaker
  • Arbor3
  • AnyPortrait
  • SRDebugger
  • Odin
  • おわりに

新規限定クーポンコード

Asset Storeで使用できる10%オフクーポンのクーポンコードが届きました。

届いたメールにnew costomersと記載されていたので、アセットストアで新規にアセットを購入する人が対象のキャンペーンコードみたいです。 自分は以前に購入したことがあるため、クーポンコードを入力しても適用されませんでした。

assetstore.unity.com

10%オフになるキャンペーンコードは 10OFF2020 です。 期間は1月31日の23時59分まで(米国標準時)ですのでお早めに使用してください!

ついでと言っては何ですが、評判が高いアセットや、おすすめのアセットを紹介しようと思います。

おすすめアセット紹介

続きを読む

【レビュー】初フルワイヤレスイヤホンとしてPowerbeats Proを購入したので良い点・悪い点をレビューしてみる

  • はじめに
  • Powerbeats Proの良い点
    • 装着時のデザイン
    • iPhoneとの連携
    • 操作性
    • バッテリー
  • 悪い点
    • 耳が痛くなる
    • 充電ケース
    • 音質
  • 終わりに

はじめに

最近、自分の周りでワイヤレスイヤホンを使用する人がかなり増えました。

職場ではAirpods Proを常に耳につけて仕事している人がいたり、

キャンプにいった時には、メンバーの大半がBeats Xを首から下げていたり、

その他にも何人かの知り合いからワイヤレスイヤホンを見せつけられました………

そうこうしているうちに、自分も欲しくなり、色々探し回った結果、Powerbeats Proを購入しました。

今回はこの製品を1週間程度使用してみて、良い点・悪い点をレビューしてみようと思います。

Powerbeats Proの良い点

装着時のデザイン

僕が、Powerbeats Proを選んだ一番の理由はずばり、この記事にあります!

e-earphone.blog

タイトルにはPowerbeats Proの記載がありませんが、この記事はAirpods Proと他のフルワイヤレスイヤホンを比較する記事です。

続きを読む

【Unity】UnityでSpriteAtlasの差分が出る問題の修正が順次適用されるらしい

  • はじめに
  • SpriteAtlasの差分問題とは
  • 公式の修正対応

はじめに

以前、SpriteAtlasの差分問題を記事にしました。

www.yui-tech-blog.com

この問題ですが、Unity ForumでUnity Technologiesの開発者が修正したよ!とコメントしていたので記事にします。

SpriteAtlasの差分問題とは

SpriteAtlasの差分問題とは、SpriteAtlasを変更していないのにもかかわらず、プレイボタンを押すと差分が出てしまう問題のことです。

複数人で共同開発しているプロジェクトで起きる問題であり、SpriteAtlasの変更をGitにプッシュした後に、他のパソコンでPullしてプレイボタンを押すと、SpriteAtlasのhash値が変わってしまいます。

また、Platformを切り替えた際にも起きていました。

続きを読む