Unityのスクリプトから RenderTexture をリアルタイムに動的生成・破棄する処理を実装することは多々あります。しかし、この生成と破棄に考慮漏れがあると、ゲームを起動してから時間が経つにつれてGPUのビデオメモリ(VRAM)が蓄積され、最終的にはメモリ不足(Out of Memory)によってアプリがクラッシュするメモリリーク問題を引き起こします。

本記事では、このRenderTextureの動的処理における「サイレントリーク」の発生メカニズムと、安全にメモリを回収する正しいクリーンアップ手法について詳細に解説します。

初心者向け:この問題を現実で例えると?

このRenderTextureのメモリリーク問題を、飲食店の「使い捨ての紙コップとゴミ箱」に例えてみましょう。

C#で new RenderTexture(...) と記述してレンダーテクスチャを作るのは、店員が「ドリンク(描画データ)を入れるための紙コップを新しく用意する」ようなものです。飲み終わった後、この紙コップは当然ゴミ箱に捨てて処分しなければなりません。

しかし、RenderTextureはヒープメモリではなく、GPUのメモリ(VRAM)という特別な場所に実体が確保されます。

もし破棄する際、スクリプトで変数に null を代入しただけで済ませてしまうと、これは「飲み終わったコップをテーブルの上に置いたまま、持ち主(変数)だけが帰宅した状態」と同じです。持ち主がいないため誰のコップか分からず、ガベージコレクターもGPUの上を勝手に片付けてくれないため、使用済みのコップがテーブルにどんどん溜まっていきます。最終的にはテーブルがコップで埋め尽くされ、お店(ゲーム)は満席で営業停止(クラッシュ)してしまいます。防ぐには、コップを捨てる前に「中身を空にして利用停止を宣言し(Release)」、その上で「コップを物理的にゴミ箱へ廃棄する(Destroy)」必要があります。

不具合の症状と具体的な現象

RenderTextureのメモリリークが発生した際、以下のような症状が見られます。

  • Profilerの Memory 項目内の RenderTexture の使用量やVRAMのグラフが右肩上がりに上昇し続け、決して下がらない。
  • 実機(特にメモリ制限の厳しいスマートフォンやQuest)でプレイした際、特定の演出を繰り返すうちに動作が著しく重くなり、最終的にアプリがクラッシュする。
  • 通常のGCが走ってもGPU側のVRAMが解放されないため、一見すると原因不明のバグに見える。

想定される原因:なぜメモリが解放されないのか?

RenderTextureは、C#のラッパーオブジェクトと、GPU側のハードウェアバッファ(テクスチャ実体)の2つから構成されています。 通常のC#クラスであれば参照が失われればGCによって回収されますが、GPU側のバッファはアンマネージド領域に属しているため、C#の参照を切るだけでは確保されたまま残り続けてしまいます。

GPU側のバッファを解放するには、明示的に Release() を呼び出してグラフィックスカードへ「このバッファはもう使いません」という通知を送る必要があります。また、C#側のラッパーメモリを解放するために Destroy() を呼ぶことも必要であり、この両方を行わない限りメモリは漏れ続けます。

具体的な解決方法とアプローチ

RenderTextureを動的に生成し、不要になった段階で完全にメモリから消滅させるための正しいクリーンアップの実装例です。

1. 正しいクリーンアップ関数の実装

using UnityEngine;

public class DynamicTextureManager : MonoBehaviour
{
    private RenderTexture dynamicRT;

    void CreateTexture()
    {
        ReleaseRenderTexture();
        dynamicRT = new RenderTexture(1024, 1024, 16, RenderTextureFormat.ARGB32);
        dynamicRT.Create();
    }

    private void ReleaseRenderTexture()
    {
        if (dynamicRT != null)
        {
            // 1. GPU側の割り当てバッファを即座に解放する
            dynamicRT.Release();

            // 2. C#側のラッパーオブジェクトを破棄する
            if (Application.isPlaying)
            {
                Destroy(dynamicRT);
            }
            else
            {
                DestroyImmediate(dynamicRT);
            }

            dynamicRT = null;
            Debug.Log("RenderTexture has been safely released.");
        }
    }

    void OnDestroy()
    {
        ReleaseRenderTexture();
    }
}

2. RenderTexture.GetTemporary によるバッファ再利用の検討

もし、短時間だけ一時的なテクスチャを使用したい場合は、毎フレーム new するのではなく、一時バッファプールから借りる手法(RenderTexture.GetTemporary)を採用します。使用後は必ず RenderTexture.ReleaseTemporary でプールに返却します。

// プールから一時テクスチャを借りる
RenderTexture tempRT = RenderTexture.GetTemporary(512, 512, 0);

// 描画処理などの実行...

// 使用後は必ずプールへ返却する(※破棄してはいけない)
RenderTexture.ReleaseTemporary(tempRT);

実務で役立つ確認チェックリスト

RenderTextureによるメモリリークを防ぐための実務チェックリストです。

確認項目 確認・対処手順
OnDestroyでの解放 スクリプトを持つGameObjectが破棄された際、OnDestroy 内で確実に解放処理が走っているか。
GetTemporaryとReleaseの対 GetTemporary を使用した場合、確実に ReleaseTemporary が同等の経路で呼ばれているか。
Profilerでの監視 Profilerの Memory を実行し、RenderTexture の数が時間経過で増え続けていないか確認する。

まとめ

RenderTextureの動的生成は、リッチなグラフィックス表現に不可欠ですが、アンマネージド領域のメモリ解放は開発者の責任で行う必要があります。Release()Destroy() を組み合わせた二段階のクリーンアップ、または RenderTexture.GetTemporary によるプール管理を徹底することで、VRAMリークのないゲームを構築しましょう。