神様は有休消化中です。

Unity関連の技術ネタを書いてます。Graphics得意にしているのでその辺の話が多めです。

【Unity】カメラ1つでUI解像度を維持し、3D解像度だけを下げる方法

昔からモバイル端末の解像度は狂気の沙汰としか言えず、あの小さな面積にフルHDとかのディスプレイを積んでいます。
撮った写真を綺麗に見るなら良いのかもしれませんが、1フレーム16msとか33msで描画しないといけないゲームではかなり辛いものがあります。
正直、Androidの解像度合戦がなければ、もっとシェーダーリッチなゲームが世の中に溢れていると思うわけです。
とはいえ世の流れですので、我々エンジニアは工夫を凝らしてよりリッチな見た目が出せるよう日々邁進するわけであります。

上記を踏まえて言いたいことは、
ゲームの解像度落としても良いよね?
ということです。

しかし、UI解像度を720pまで落としてしまうと途端にユーザーにバレてしまいます。
UIは結構解像度の劣化が目立つんですよね。
しかし、3D空間側の解像度を落としてもほとんど目立たず、フィルレートが稼げて美味しいのです。
私の尊敬するエンジニアの1人、Unityの安原さんもUnity 2017 Tokyoで同じことをおっしゃっています。

【Unite 2017 Tokyo】スマートフォンでどこまでできる?3Dゲームをぐりぐり動かすテクニック講座

3D側の解像度だけを下げる

さて、やっと本題です。
やりたいことは、「UI解像度は高く」「3D解像度は低く」レンダリングです。
簡単に言うと3D描画を自前で用意したRenderTextureに描画して、3D描画が終わったらBack BufferにBlitすれば良いことになります。
Google先生に聞いてみると、以下のような記事がヒットしました。
wordpress.notargs.com
この記事の中では、Back BufferにBlitする用のカメラを用意して、そのカメラにCommandBufferをAddすることでRenderTextureをBackBufferに書き込んでいます。
これは、同一カメラでのCommandBufferではAfterImageEffects, AfterEveryThingでのSetRenderTargetでBack Bufferを指定出来ないからだと認識していました。事実私もこの記事を参考にいろいろ試したのでですが指定出来ず、以前の記事ではこの方式で実装しています。
appleorbit.hatenablog.com


しかし、しかしです!
本当に偶然ではあるのですが、以下の記事をみつけました。
Unity 5: AfterImageEffects and AfterEverything Traps – Updated My Journal.

全CameraEvent共通で、SetRenderTarget( -1 )と指定するとBack Bufferがセットされる(RenderTargetIdentifer( int nameID )でそうなっている?)

本当にこのブログの著者様には感謝の念しかありません。
というわけで、書いてみました。
以下のコードで、3D解像度だけを1つのカメラで下げることができます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;


/// <summary>
/// 解像度を変更するコンポーネント
/// </summary>
[RequireComponent(typeof(Camera))]
public class ResolutionConverter : MonoBehaviour
{
    /// <summary>
    /// 解像度
    /// </summary>
    public enum Resolution
    {
        None,
        HD,
        FULLHD
    }

    /// <summary>
    /// 解像度設定
    /// </summary>
    [SerializeField] private Resolution m_Resolution = Resolution.None;

    /// <summary>
    /// ターゲットカメラ
    /// </summary>
    private Camera m_Camera;
    /// <summary>
    /// フレームバッファ
    /// </summary>
    private RenderTexture m_FrameBuffer;
    /// <summary>
    /// コマンドバッファ
    /// </summary>
    private CommandBuffer m_CommandBuffer;

    /// <summary>
    /// 初期化
    /// </summary>
    private void Awake()
    {
        m_Camera = GetComponent<Camera> ();

        Apply (m_Resolution);
    }

    /// <summary>
    /// 適用する
    /// </summary>
    public void Apply(Resolution resolution)
    {
        m_Resolution = resolution;

        if (resolution == Resolution.None) {
            return;
        }

        var size = GetResolutionSize (resolution);
        size = FitScreenAspect (Screen.width, Screen.height, size.x, size.y);
        UpdateFrameBuffer (size.x, size.y, 24);
        UpdateCameraTarget ();
        AddCommand ();
    }


    /// <summary>
    /// フレームバッファの更新
    /// </summary>
    private void UpdateFrameBuffer(int width, int height, int depth, RenderTextureFormat format = RenderTextureFormat.Default)
    {
        if (m_FrameBuffer != null) {
            m_FrameBuffer.Release ();
            Destroy (m_FrameBuffer);
        }

        m_FrameBuffer = new RenderTexture (width, height, depth, format);
        m_FrameBuffer.useMipMap = false;
        m_FrameBuffer.Create ();
    }

    /// <summary>
    /// カメラの描画先を更新
    /// </summary>
    private void UpdateCameraTarget()
    {
        if (m_FrameBuffer != null) {
            m_Camera.SetTargetBuffers (m_FrameBuffer.colorBuffer, m_FrameBuffer.depthBuffer);
        } else {
            m_Camera.SetTargetBuffers (Display.main.colorBuffer, Display.main.depthBuffer);
        }
    }


    /// <summary>
    /// 解像度の実サイズを取得
    /// </summary>
    private Vector2Int GetResolutionSize(Resolution resolution)
    {
        bool isPortrait = Screen.height > Screen.width;

        switch(resolution){
        case Resolution.HD:
            if (isPortrait) {
                return new Vector2Int (720, 1280);
            }
            return new Vector2Int (1280, 720);
        case Resolution.FULLHD:
            if (isPortrait) {
                return new Vector2Int (1080, 1920);
            }
            return new Vector2Int (1920, 1080);
        }

        return new Vector2Int (Screen.width, Screen.height);
    }


    /// <summary>
    /// 解像度の計算
    /// </summary>
    private static Vector2Int FitScreenAspect(int width, int height, int maxWidth, int maxHeight)
    {
        // 解像度以下なら何もしない
        if (width <= maxWidth && height <= maxHeight) {
            return new Vector2Int (width, height);
        }

        if (width > height) {
            float aspect = height / (float)width;
            int w = Mathf.Min (width, maxWidth);
            int h = Mathf.RoundToInt (w * aspect);

            return new Vector2Int (w, h);
        }

        {
            float aspect = width / (float)height;
            int h = Mathf.Min (height, maxHeight);
            int w = Mathf.RoundToInt (height * aspect);

            return new Vector2Int (w, h);
        }
    }

    /// <summary>
    /// コマンドを追加する
    /// </summary>
    private void AddCommand()
    {
        RemoveCommand ();

        // カラーバッファをバックバッファ(画面)に描きこむコマンド
        {
            m_CommandBuffer = new CommandBuffer ();
            m_CommandBuffer.name = "blit to Back buffer";

            m_CommandBuffer.SetRenderTarget (-1);
            m_CommandBuffer.Blit (m_FrameBuffer, BuiltinRenderTextureType.CurrentActive);

            m_Camera.AddCommandBuffer (CameraEvent.AfterEverything, m_CommandBuffer);
        }
    }
    /// <summary>
    /// コマンドを破棄する
    /// </summary>
    private void RemoveCommand()
    {
        if (m_CommandBuffer == null) {
            return;
        }
        if (m_Camera == null) {
            return;
        }

        m_Camera.RemoveCommandBuffer (CameraEvent.AfterEverything, m_CommandBuffer);
        m_CommandBuffer = null;
    }
}

個人的に最近最もテンションの上がった内容でした。