複数解像度の端末でPersepectiveなカメラの画面いっぱいにSpriteを広げてみた

AvatarPosted by

はじめに

こんにちは。クライアントエンジニアの鈴木です。

今回は様々な解像度の端末で(Persepectiveなカメラを使用した3D空間上の)Spriteを画面いっぱいに広げて欲しいという要望があり、実装してみました。Orthographicなカメラでの調整方法はブラウザで検索するとすぐに出てきますが、Persepectiveなカメラで3D空間上のSpriteを調整する方法についてはあまり出てこないので記事にしてみることにしました。

結果

様々な解像度に変化させても画面いっぱいにSpriteを表示することができました。

下記動画で以下の2点を確認できます。

  1. 解像度を変化させても画面の縦横どちらかにフィットして出来るだけ大きく全体が見えるように画像が表示される
  2. カメラを移動させてもPreviewが同じ見え方になる

解像度を変化させたりカメラを移動させたりした

方針

まずSpriteを画面いっぱいに広げるというのが抽象的なので少し具体化してみます。以下3つ考えてみました。

  1. Spriteの縦横を画面に合わせる
  2. Spriteのtexture画像のアスペクト比を保って画面いっぱいに広げる
  3. Spriteをある基準のアスペクト比にスケールしてから画面いっぱいに広げる

1はアスペクト比のことを考えていないので、端末によっては縦または横にバランス悪く引き伸ばされて表示される可能性が高いです。

Spriteの縦横を画面に合わせる

それに対して2は画像側のアスペクト比を読み取り、保ったまま画面いっぱいまで広げるため、バランス良く拡縮された画像が表示されます。

Spriteのtexture画像のアスペクト比を保って画面いっぱいに広げる

3は一見うまく行かなそうに見えますが、デザイン時の画像のアスペクト比が基準で揃えてあるなら成立します。実はこの方法であればある程度のアスペクト比の違いがあっても圧縮を効かせることが可能です。デザインツールから画像出力時に2のべき乗の解像度に書き出し、スクリプト側で再度デザイン時のアスペクト比に戻して表示すれば、あまり画像を劣化させずに容量を圧縮することが出来ます。

Spriteをある基準のアスペクト比にスケールしてから画面いっぱいに広げる

実装

今回は画像素材が2のべき乗でなかったため、方針3の方法で実装してみました。画像は基準の解像度でデザインされていることを前提としています。

下記は全体の実装コードです。

using UnityEngine;

public class SpriteBgScaleInitializer : MonoBehaviour
{
    [SerializeField]
    private SpriteRenderer _spriteRenderer;
    
    [SerializeField]
    private Camera _camera;

    private void Update()
    {
        // 基準解像度
        var baseWidth = 1138f;
        var baseHeight = 640f;

        // Spriteサイズ
        var spriteSize = _spriteRenderer.sprite.bounds.size;
        
        // 基準アスペクト比
        var baseAspectRatioHeight = baseHeight / baseWidth;

        // 画面アスペクト比
        var screenAspectRatioHeight = (float) Screen.height / Screen.width;

        // デザイン時のSpriteサイズ[unit]
        var designedSpriteSize = new Vector2(spriteSize.x, spriteSize.x * baseAspectRatioHeight);

        // scaleを変化させてデザイン時のアスペクト比に加工
        var targetScale = new Vector3(1f, designedSpriteSize.y / spriteSize.y, 1f);

        // カメラTransform
        var cameraTransform = _camera.transform;
        
        // 物体のカメラ方向正射影ベクトルの長さ
        var projectPos = cameraTransform.position + Vector3.Project(transform.position - cameraTransform.position, cameraTransform.forward);
        var projectDistance = Vector3.Distance(cameraTransform.position, projectPos);

        // 物体のカメラ方向正射影ベクトル
        var projectVector = projectDistance * cameraTransform.forward.normalized;

        // 縦長の画像か
        var isPortraitImage = designedSpriteSize.y / designedSpriteSize.x > screenAspectRatioHeight;
        
        // 縦長なら横で合わせる
        if (isPortraitImage)
        {
            // 水平FOV
            var fovRad = Mathf.Atan(Mathf.Tan(_camera.fieldOfView / 2f * Mathf.Deg2Rad) / screenAspectRatioHeight) * 2f;

            // カメラからSpriteの位置での画面幅を取得(正弦定理)
            var width = 2 * projectDistance * Mathf.Sin(fovRad / 2) / Mathf.Sin((Mathf.PI - fovRad) / 2);

            targetScale = targetScale * width / designedSpriteSize.x;
        }
        else
        {
            // 垂直FOV
            var fovRad = _camera.fieldOfView * Mathf.Deg2Rad;

            // カメラからSpriteの位置での画面幅を取得(正弦定理)
            var height = 2 * projectDistance * Mathf.Sin(fovRad / 2) / Mathf.Sin((Mathf.PI - fovRad) / 2);

            targetScale = targetScale * height / designedSpriteSize.y;
        }

        // ビルボードで表示
        transform.position = projectVector + cameraTransform.position;
        transform.rotation = cameraTransform.rotation;
        
        // 基準のアスペクト比で画面いっぱいに広がるようにスケール
        transform.localScale = targetScale;
    }
}

順に解説します。

基準のアスペクト比にスケールする

まず、基準のアスペクト比を1138×640として任意のサイズ(今回は1024×1024)の画像を1138×640のアスペクト比に引き延ばします。

        // 基準解像度
        var baseWidth = 1138f;
        var baseHeight = 640f;

        // Spriteサイズ
        var spriteSize = _spriteRenderer.sprite.bounds.size;
        
        // 基準アスペクト比
        var baseAspectRatioHeight = baseHeight / baseWidth;

        // 画面アスペクト比
        var screenAspectRatioHeight = (float) Screen.height / Screen.width;

        // デザイン時のSpriteサイズ[unit]
        var designedSpriteSize = new Vector2(spriteSize.x, spriteSize.x * baseAspectRatioHeight);

        // scaleを変化させてデザイン時のアスペクト比に加工
        var targetScale = new Vector3(1f, designedSpriteSize.y / spriteSize.y, 1f);

Spriteのアスペクト比を基準に合わせる

横または縦に合わせる

カメラとSpriteの位置関係から画像のアスペクト比を保ちながら画面いっぱいに広げます。

        // カメラTransform
        var cameraTransform = _camera.transform;
        
        // 物体のカメラ方向射影ベクトルの長さ
        var projectPos = cameraTransform.position + Vector3.Project(transform.position - cameraTransform.position, cameraTransform.forward);
        var projectDistance = Vector3.Distance(cameraTransform.position, projectPos);

        // 物体のカメラ方向射影ベクトル
        var projectVector = projectDistance * cameraTransform.forward.normalized;

        // 縦長の画像か
        var isPortraitImage = designedSpriteSize.y / designedSpriteSize.x > screenAspectRatioHeight;
        
        // 縦長なら横で合わせる
        if (isPortraitImage)
        {
            // 水平FOV
            var fovRad = Mathf.Atan(Mathf.Tan(_camera.fieldOfView / 2f * Mathf.Deg2Rad) / screenAspectRatioHeight) * 2f;

            // カメラからSpriteの位置での画面幅を取得(正弦定理)
            var width = 2 * projectDistance * Mathf.Sin(fovRad / 2) / Mathf.Sin((Mathf.PI - fovRad) / 2);

            targetScale = targetScale * width / designedSpriteSize.x;
        }
        else
        {
            // 垂直FOV
            var fovRad = _camera.fieldOfView * Mathf.Deg2Rad;

            // カメラからSpriteの位置での画面幅を取得(正弦定理)
            var height = 2 * projectDistance * Mathf.Sin(fovRad / 2) / Mathf.Sin((Mathf.PI - fovRad) / 2);

            targetScale = targetScale * height / designedSpriteSize.y;
        }

カメラの見える範囲にフィットさせる

ビルボードで画面いっぱいに広げる

最後に計算結果を使ってビルボードで画面いっぱいに広げました。

        // ビルボードで表示
        transform.position = projectVector + cameraTransform.position;
        transform.rotation = cameraTransform.rotation;
        
        // 基準のアスペクト比で画面いっぱいに広がるようにスケール
        transform.localScale = targetScale;

終わりに

今回は画像が同じ解像度でデザインされていることを前提とし、複数解像度の端末でPersepectiveなカメラの画面いっぱいにSpriteを広げてみました。しかし、違う解像度でデザインされている場合も2のべき乗サイズの画像としてデザインされていれば上の実装を少し変えるだけで実現できそうです。参考にしていただけると幸いです!