Unity テクスチャのメモリ使用量をプラットフォーム別に正確に求める

本記事は、「サムザップ Advent Calendar 2025」 の12/6の記事です。

はじめに

複数プラットフォームを対象にしたUnityプロジェクトでは、テクスチャが実機でどれだけメモリを消費するかを把握することが欠かせません。Inspectorを見ればメモリ使用量はわかるものの、プラットフォームごとのメモリ使用量はSwitchPlatformが必要になって調査に時間がかかってしまいます。
また、「Prefabやシーンが含む全てのテクスチャのメモリ使用量を試算したい」といったことはよくあると思いますが、これには依存するテクスチャのプラットフォームごとのメモリ使用量を算出できる必要があります。
そこでこの記事では「プラットフォームを切り替えずに、実機と同じフォーマットを前提に試算する」ためのメモリの試算方法をまとめてみます。

ポイント:

  • 非圧縮32bitでの試算方法
  • ブロック圧縮
  • mipmapと2D / 2DArray / 3Dの計算方法と比較
  • Read/Write

非圧縮(32bit)テクスチャ

まずは非圧縮な32bitテクスチャ (RGBA32) を考えてみます。
計算式はシンプルに width * height * 4byte となります。

ブロック圧縮

次に、多くのモバイル/PCプラットフォームで使用されているブロック圧縮を考えます。
4×4 ピクセルなどを1ブロックとして決まったビット数に圧縮する方式です。
合計ブロック数をtotalBlocks、ブロックごとのビット数をbitsPerBlockとそれぞれおくと、テクスチャのメモリ使用量は totalBlocks * bitsPerBlock / 8 で算出できます(非圧縮の「ピクセル数 × 32bit ÷ 8」と対応する形です)。

ブロック圧縮DXT1の例

4×4 = 16px のブロックを RGBA32 なら 64byte で持つところ、DXT1 では 64bit(8byte)で表現するので 1/8 まで圧縮されます。

非圧縮 (RGBA32)
-------------------------------------
| R,G,B,A | R,G,B,A | ... (16 px)  |
-------------------------------------
 1px = 32bit
 16px = 16 * 32bit = 512bit (64byte)

ブロック圧縮 (例: DXT1 / DXT5 / ASTC)
-------------------------
| 4×4 pixel block (16px) |
-------------------------
= 64bit (DXT1)
= 128bit (DXT5 / ASTC / BC7)

■ 比較
非圧縮:    512bit
DXT1圧縮:   64bit   (1/8)
DXT5圧縮:  128bit   (1/4)
ASTC等:    128bit   (1/4)

ブロックサイズで割り切れない場合

圧縮はブロック単位なので端数は切り上げます。例えば幅 30px で blockSize=4 なら ceil(30/4)=8 ブロック扱いとなり、実質 32px として計算されます。
DXT / BC 系は 4 の倍数でないと圧縮できず、非圧縮へフォールバックすることがある点にも注意します。

幅 = 30px
blockSize = 4px

30 / 4 = 7.5 → ceil = 8 ブロック必要

実際のブロック割当(横方向)
+---+---+---+---+---+---+---+---+
| B | B | B | B | B | B | B | B |
+---+---+---+---+---+---+---+---+
  1   2   3   4   5   6   7   8   ← 8 blocks (= 32px 相当)

実テクスチャ 30px は、
ブロック圧縮上は 32px 分として扱われる。

フォーマット別の blockSize と bitsPerBlock

Format blockSize bitsPerBlock 備考
RGB24 1 24 非圧縮
RGBA32 1 32 非圧縮
DXT1 4 64 4×4 圧縮
DXT5 4 128 4×4 圧縮 α8bit
ASTC 4×4 4 128 1 ブロック固定
ASTC 6×6 6 128 同上
ETC_RGB4 4 64
PVRTC_RGB4 4 64
BC7 4 128

DXT / BC のフォールバック(事故ポイント)

サイズが 4 の倍数でないと圧縮不可のため、Unity は非圧縮にフォールバックします。具体的には DXT1→RGB24、DXT5→RGBA32、BC4/5/6H→RGB24、BC7→RGBA32。
1023×1023 を DXT5 想定で作ると非圧縮 RGBA32 になり、8 倍以上のメモリになるという事故が起きがちです。
特に、モバイル向けにアプリをリリース後に、追加でPC向けにもリリースしたい!となった場合に既存のテクスチャが4の倍数になっていないことで問題になることが多いので注意が必要です。

各フォーマットにおける mipmap のメモリ使用量への影響

基本の考え方

mipmap は Unity が自動で作る縮小版の集合で、width/height を 1/2 ずつにしたものが最後の 1×1 まで続く仕組みです。
各 mip のメモリを足し合わせれば、mipmap 込みのメモリ使用量を計算できます。

※参考:https://docs.unity3d.com/ja/2021.3/Manual/texture-mipmaps-introduction.html

Texture2D

depth は 1 なので、幅と高さだけを半減させながらブロック数を積み上げます。
式にするとtotalBlocks += ceil(mipWidth/blockSize) * ceil(mipHeight/blockSize)です。
メモリはtotalBytes = totalBlocks * bitsPerBlock / 8です。

mip0:  16 × 16
mip1:   8 ×  8
mip2:   4 ×  4
mip3:   2 ×  2
mip4:   1 ×  1  ← 最終レベル

メモリ使用量は全てのmipのメモリ使用量の和

Texture2DArray(depth = スライス数)

Texture2DArrayはTexture2Dがスライス数分だけ連なっているものなので、メモリはtotalBytes = Texture2D一枚分のメモリ使用量* depthとなります。

 Texture2DArray
  Slice0: mip0 + mip1 + ... + 1x1
  Slice1: mip0 + mip1 + ... + 1x1
  Slice2: mip0 + mip1 + ... + 1x1
  ...

メモリ使用量は Texture2Dの1枚分のメモリ使用量 × depth

Texture3D

depth にも mipmap がかかり、幅・高さ・奥行きを半減させながらブロック数を積み上げます。
totalBlocks += ceil(mipWidth/blockSize) * ceil(mipHeight/blockSize) * mipDepth となります。
メモリは totalBytes = totalBlocks * bitsPerBlock / 8です。

mip0:  16 × 16 × 16
mip1:   8 ×  8 ×  8
mip2:   4 ×  4 ×  4
mip3:   2 ×  2 ×  2
mip4:   1 ×  1 ×  1  ← 最終レベル

  ↑ width/height だけでなく
    depth 方向にも mip がかかる点が重要

メモリ使用量は 全てのmipのメモリ使用量の和

Read/Write Enabled の影響

GPU 用テクスチャに加えて CPU 側バッファを持つため、Read/Write Enabled が true ならメモリは単純に 2 倍になります。

メモリ計算コードの全容

最後に、ここまで書いたことを合わせて、プラットフォーム設定からフォーマットと最大サイズを取得し、blockSize / bitsPerBlock で積み上げる処理を記載します。

// 3D テクスチャ: depth も mip 計算に含める
private long CalculateTexture3DMemorySize(Texture3D texture)
{
    return CalculateTextureMemorySize(texture, texture.width, texture.height, texture.depth);
}

// 2DArray: スライス数(depth)を最後に掛ける
private long CalculateTexture2DArrayMemorySize(Texture2DArray texture2DArray)
{
    return CalculateTextureMemorySize(texture2DArray, texture2DArray.width, texture2DArray.height, 1) *
           texture2DArray.depth;
}

// 2D: depth=1 でそのまま計算
private long CalculateTextureMemorySize(Texture texture)
{
    return CalculateTextureMemorySize(texture, texture.width, texture.height, 1);
}

// テクスチャのメモリ計算本体
private long CalculateTextureMemorySize(Texture texture, int textureOriginalWidth, int textureOriginalHeight,
                                        int depth)
{
    // インポーターから実際に使われるフォーマットと最大サイズを取得
    var (textureFormat, textureMaxSize) = GetTextureFormatAndMaxSize(texture);

    // 最大サイズを超える場合は実際に使われるサイズにクランプ
    var originalWidth = Math.Min(textureMaxSize, textureOriginalWidth);
    var originalHeight = Math.Min(textureMaxSize, textureOriginalHeight);

    // フォーマットから blockSize(1 辺のピクセル数)と bitsPerBlock(1 ブロックのビット数)を得る
    var (blockSize, bitsPerBlock) = GetBitData(textureFormat);

    // 各 mip のブロック数を積み上げる
    long totalBlocks = 0;
    for (var mip = 0; mip < texture.mipmapCount; mip++)
    {
        // 各 mip の解像度。3D の場合 depth も 1/2 ずつ減る
        var mipWidth = Mathf.Max(1, originalWidth >> mip);
        var mipHeight = Mathf.Max(1, originalHeight >> mip);
        var mipDepth = Mathf.Max(1, depth >> mip);

        // ブロック数を切り上げで計算(割り切れない場合はパディング相当)
        var numOfBlocks = Mathf.CeilToInt((float)mipWidth / blockSize) *
                          Mathf.CeilToInt((float)mipHeight / blockSize);

        // 3D では mipDepth を掛ける。2D/2DArray は depth=1 or スライス数で呼び出し側が掛ける
        totalBlocks += numOfBlocks * mipDepth;

        // 1×1 まで到達したら終了
        if (mipWidth == 1 && mipHeight == 1)
        {
            break;
        }
    }

    // ブロック数 × フォーマットのビット数で総ビット数を求める
    var totalSize = totalBlocks * bitsPerBlock;

    // Read/Write Enabled の場合は CPU/GPU の二重保持になるので 2 倍
    if (IsReadWriteEnabled(texture))
    {
        totalSize *= 2;
    }

    // ビットをバイトに換算して返す(端数切り上げ)
    return Mathf.CeilToInt(totalSize / 8.0f);
}

// 各 mip のブロック数を合計するユーティリティ
private long CalcBlocks2D(int width, int height, int mipCount, int blockSize)
{
    long totalBlocks = 0;
    for (var mip = 0; mip < mipCount; mip++)
    {
        var mipWidth = Math.Max(1, width >> mip);
        var mipHeight = Math.Max(1, height >> mip);
        totalBlocks += Mathf.CeilToInt(mipWidth / (float)blockSize) *
                       Mathf.CeilToInt(mipHeight / (float)blockSize);

        if (mipWidth == 1 && mipHeight == 1)
        {
            break;
        }
    }
    return totalBlocks;
}

GetTextureFormatAndMaxSize はターゲットプラットフォーム設定からフォーマットと最大サイズを取得し、GetBitData は前節の対応表に基づいて blockSize / bitsPerBlock を返します。
CalcBlocks2D は各 mip の blocksX×blocksY を合計するヘルパー関数です。

まとめ

計算の芯は「ブロック数(ceil(width/blockSize)×ceil(height/blockSize) を mip ごとに合算)× bitsPerBlock ÷ 8」を積み上げるだけです。
ただし、DXT/BCなど ブロック圧縮の一部フォーマット では割り切れない解像度が非圧縮に落ちる点は注意が必要です。
mipmap は 2D/2DArray/3D で扱いが変わり、特に 2DArray がフルの mip を各スライスで持つぶん重くなりやすいことを意識します。
Read/Write Enabled が true なら CPU/GPU 二重保持で 2 倍になるので、最後に必ず確認します。
この流れをコードに落とし込めば、SwitchPlatform なしでプラットフォーム別のメモリ使用量を再現でき、Prefab や Scene が抱える総テクスチャメモリも自動で見積もることができます。
読んでいただいた方の制作や検証の一助になればうれしいです。


二宮 章太

株式会社サムザップ クライアントエンジニア 2015年入社。
主に新規タイトルの開発に従事し、『呪術廻戦ファントムパレード』のリリースに携わる。
週末は子どもと公園で遊んで筋肉痛になっています。