Unityのオフスクリーンレンダリング

  • 65
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

Unityで所謂オフスクリーンレンダリングを実現するには、Unity ProのRenderTextureを使う。

offscreen

使い方は簡単で、Target Textureに設定するとCameraの描画先が画面ではなくオフスクリーン(画面裏)になる。 一旦描画した後はRenderTextureに転送しているCameraを停止させれば、Textureの描画はストップする。 この際、Cameraが描画しているテクスチャを削除・非表示にしてしまえば、画面に写さず特定のスプライトの塊を描画できる。 

4.3以前はSceneの変更やスリープ等で破棄されていたが、4.3で改善され使い勝手が良くなった。(以前のバージョンでは、スリープに関してはスリープ解除時に再取得すればよかったが、シーン切替時に前シーンの画面を残すが出来なかったのでTexture2Dに変換するといった面倒な事をする必要があった。もしくは1シーン管理)

public class BackgroundRender : MonoBehaviour
{
    [SerializeField, Range(1, 10)]
    private int scale = 1;

    public RenderTexture texture{ get; private set; }

    void Awake()
    {
        texture = new RenderTexture(
            Screen.width / scale, 
            Screen.height / scale, 24);
        texture.enableRandomWrite = false;
        camera.targetTexture = texture;
    }

    void OnPostRender()
    {
        gameObject.SetActive(false);
    }
}

一応Unity freeでも似たような事をする方法があるので紹介しておこうと思う。ただ、この方法は非常にパフォーマンスが非常に悪いので、その辺りに注意が必要だ。

やり方は単純で、メインカメラより早いタイミングでカメラをレンダリングし、結果をOnPostRenderのタイミングでReadPixelsでTexture2Dに流し込むだけ。このタイミングで取得すれば、Cameraが描画した直後の画像が取得できるので、カメラのレンダリング結果だけを取得できる。

https://gist.github.com/tsubaki/7789119

using UnityEngine;
using System.Collections;
using System.IO;
using System;

public class BackgroundRenderBasic : MonoBehaviour
{
    public Texture2D screenshot { get; private set; }
    RenderTexture renderTexture = null;
    public Action result = null;

    [Range(1, 5)]
    public int
        textureScale = 1;

    void Awake()
    {
        int width = Screen.width / textureScale;
        int height = Screen.height / textureScale;
        screenshot = new Texture2D(width, height, TextureFormat.RGB24, false);
        renderTexture = new RenderTexture(width, height, 24);
        camera.targetTexture = renderTexture;
    }

    void OnPostRender()
    {
        Take();

        if (result != null)
            result();

        gameObject.SetActive(false);
    }

    protected void Take()
    {
        screenshot.ReadPixels(new Rect(0, 0, Screen.width, Screen.height), 0, 0);
        screenshot.Apply();
    }

    void OnDestroy()
    {
        Destroy(renderTexture);
        Destroy(screenshot);
        result = null;
    }
}

ただし、前述した通りfree版でも出来る方法は非常にパフォーマンスが悪い。どうしてもパフォーマンスが欲しい場合、描画先のTextureのクォリティを下げるといった対策が必要になる。 もしくは、ReadPixelsの範囲を絞る等の取得し描画するピクセルの範囲を絞る工夫も良いかもしれない。

個人的には、スライドアウトのようなエフェクトに使う場合は1/3くらいにしてもバレないように思う(ケースバイケースだが)。


オフラインレンダリングを少し試してみた所、確かに背景には使い勝手が良いかもしれない。

  1. フィルレートを節約出来る
  2. 負荷の高いレンダリング結果を使用出来る

背景のような大きく画面をうめつくす画像は、非常にフィルレートの負荷になりやすい。例えば画像のように木があり背景があり、しかも透明色を使っている場合、非常に多くの描画コストがかかる。

スプライト状態

この色が濃い所は非常に描画コストが高い箇所。特に解像度が高い端末はこの部分が多いとパフォーマンスに致命的なダメージを受ける。その為、この背景スプライト部分を1箇所に合わせることで、かなり描画パフォーマンスを減らすことが出来る(その際、シェーダーはTexture等を使うと良い)。

オフラインレンダリングで統合

この背景しか行っていない理由としてもフィルレートが有る。要するに手前のオブジェクトを事前にレンダリングして描画してバッチを節約するよりも画面全体に描画してしまう問題のほうがプレッシャーが高いといった理由だ。(透明は描画しないのではなく、透明を描画している。描画しないcutouはpower vrで非常に負荷が高い)

勿論、動く手前の物に使っても良いかもしれない。ただし複数レイヤーを重ねて描画するのは非常にコストが高い。かといってオブジェクト毎にカメラを設定してレンダリングするのは頭が悪い。 そして1個のカメラで済ます場合は、全て同じ画像を扱うか、スプライトを毎フレーム作るような面倒なことをする必要がある。

ついでに、静止画なら兎も角、基本的に1回描画した画像を再利用するスタンスなので、リアルタイムで動くものには向かないのも理由の一つだ。この場合は普通に描画した方がどう考えても高速で動作する。

なので動く手前ではなく、動かない背景のみに使うといった限定的な方法で使うことになる。なおUnity2Dを使う場合はテクスチャからポリゴンを作成してフィルレートを可能な限り抑える仕組みがある。

なお、フィルレートが高い可能性のある箇所はSceneビュー上のOverDrawで確認出来る。透明を持たないTextureも透明に見えるので簡単に確認とは行かないが、少なくとも重なっている箇所位は判る。下の図はついうっかり馬鹿みたいにパーティクルを生成した時の画像。若干のオレンジ程度なら何とかなるが、白くなっている箇所が広いと負荷が非常に高い。

OverDraw

また、フィルレートの他にも1点、非常に大きな問題がある。非常に単純で、この方法は恐ろしくメモリを食う。これは背景のような巨大なテクスチャをランタイムで作成しているためだ。(ランタイムでテクスチャを作成する場合はGPUが読み込める形式…つまりRGB24のようなRAWなフォーマットを使うことになる)
例えば最近のHDクラスのスクリーン(2048x1024)を描画する場合、大体1枚につき16MBの画像を使用する。だが、一度描画に使用したテクスチャは以降使用することは無い為、破棄することもできる。ここもケースバイケース。

この投稿は Unity Advent Calendar 20137日目の記事です。