Unity メモリ使用量削減手法 (主に WebGL)

# はじめに

とあるプロジェクトで Unity に携わることになり、メモリ使用量削減に迫られていました
最初はスマホ向けのプロジェクトで、そのまま WebGL に吐き出すことができたのは感動しました
浮かれていたところ、下記のエラーに悩まされることになりました

```
Out of memory.

If you are the developer of this content, try allocating more memory to your WebGL build in the WebGL player settings.
```

WebGL では利用できるメモリが限られており、スマホやPC、Web Player(今は Web Player は廃止)などとは違い、結構シビアな環境だということが判明しました

ここでは Unity のメモリ削減に関して kaishuu0123 が行った手法について記載しています
WebGL に限ったお話ではなく、他のプラットフォームでも応用できるかもしれません
**www クラスと Texture のお話がメインです**

(注: Unity5 の頃の Tips なので今はもっと賢くなっていると思います)

## [前提条件] WebGL では GC(Garbage Collection) の発生タイミングを任意に発火させることはできない

* Android/WebPlayer の場合には `GC.Collect()` をコード内部で実行することで発火させることができた
* WebGL ではブラウザの GC に任せるため、任意のタイミングで実行はできない
    * https://blogs.unity3d.com/jp/2016/12/05/unity-webgl-memory-the-unity-heap/
    * `Calling GC.Collect() has no effect on Unity WebGL.`
* そのため、メモリ使用量に関しては気を配る必要がある

## WWW クラス利用後は Dispose() を実行して、Resources.UnloadUnusedAssets を実行する

* `www` クラスでテクスチャやオーディオの読み込みをした際にメモリが消費される
    * `www.texture` とか `www.GetAudioClip` とか
* Unity 内部の実装の詳細は不明だが、 `www` クラスで取得したものは、他の変数に移して、Dispose(メモリ破棄) し、 Unity 側にリソースの解放を依頼する方法(`Resources.UnloadUnusedAssets`)を取るとメモリ削減に繋がる
* coroutine 内部で実行している場合、 一度 `yield return` の箇所で実行してあげることで多少は削減することが可能

* `textureArray` というオブジェクトの配列を保持する変数に Texture を読み込み、解放しているサンプルコード

```c#:sample
textureArray[key] = (T)(object)www.texture; // <- ここで異なる変数に値をコピーしてあげる。参照だと駄目
www.Dispose(); // <- ここで明示的に破棄を指定

// Unity にリソースの解放を依頼。あくまで依頼なので、このタイミングで確実に解放されるわけではない
yield return Resources.UnloadUnusedAssets(); 
```

## テクスチャは 2の累乗で読み込む

* OpenGL の挙動らしい
    * NPOT(Non Power Of Two) / POT(Power Of Two) で調べるといいかも
    * WebGL はほぼ OpenGL と同様なので、テクスチャ読み込みに関しては 2の累乗になる
    * OpenGL と同じような挙動をするということは、OpenGL 関連の Hack も応用できるかもしれない
* POT (2の累乗) にした方がメモリ使用量としては親切
* 多分 Unity 内部で読み込んでいる画像サイズが2の累乗じゃないとき、2の累乗計算して、そこにマッピングしているから、という可能性がある
* 2048x2048 でテクスチャを作成して、 `www` から画像を読みだしてマッピングしているサンプルコード

```c#:sample
Texture2D texture = new Texture2D(2048, 2048, TextureFormat.DXT5Crunched, false);
www.LoadImageIntoTexture(texture);
```

これがメモリ削減になる理由としては、言葉足らずな気がするのでツラツラと思いを書くと

* 「固定サイズのテクスチャにしないで、`www` クラスからサイズ読み込めばいいじゃん」という方法は取れない
    * なぜなら、`www` クラスから画像サイズを取得したタイミングで画像がメモリ上に展開されてしまうため、削減にはつながらない
* そのため、ある程度必要になる解像度が予測できるならば、POT である特定のフォーマットのテクスチャを確保しつつ、それに対して書き込むようにしてあげれば、`www` クラスからの書き込み時に最適なメモリ使用量をコントロールすることができる

## 読み込んだテクスチャは圧縮する

* WebGL 環境の場合に限るが、画質の多少の劣化を犠牲にして圧縮を利用することができる
* png などのアルファ値がある場合、 DXT5 を選ぶとよい(他はググって調べてください)
* www.texture を直接読み込んでいた個所を、以下のようにして圧縮を行い、省メモリ化したサンプルコード

```c#:sample
// DXT5Crunched の空のテクスチャを用意
Texture2D texture = new Texture2D(2048, 2048, TextureFormat.DXT5Crunched, false);
// ここでテクスチャを読み込み
www.LoadImageIntoTexture(texture);
// テクスチャ圧縮
texture.Compress(false);
// 圧縮後のテクスチャを割り当て
textureArray[key] = (T)(object)texture;
// 読み込みは終わったので Dispose()
www.Dispose()
```

## プラットフォーム依存のテクスチャフォーマット設定を利用して、メモリ使用量を削減する

* これは Unity にデフォルトで用意されている機能を使う
    * `Unity texture format` などと調べれば出てくる
* Unity ではプラットフォーム毎(Android/WebPlayer/WebGL ...) 毎にテクスチャの圧縮設定を変えることができる
* WebGL の時だけ、DXT5Crunched を適用するようにした(元々は TrueColor が設定されていたりする)
    * 見栄えを気にする画像には適用しづらいので注意
    * 例えばアプリのトップ画面とかは映えるので注意が必要
    * 主に UI 部品とかに適用するとバレにくい？

# 参考文献

* WebGL のメモリ周りは公式が一番詳しい
    * https://blogs.unity3d.com/jp/2016/12/05/unity-webgl-memory-the-unity-heap/

# 追記

WebGL 独自の設定として、WebGL に割り当てるメモリ量を変更することができます(Unity 5の頃の話なので今もあるかは不明)

1. [Build Settings] -> [Player Settings]
2. [Inspector]でメモリを設定

感覚値としては 128 or 256 or 512 MB あたりまで割り当てられた気がします
(が、512MB はユーザにとっても辛いかも)

# まとまらないまとめ

* 最初は触れてみて「Unity WebGL すげー！Emscripten すげー!」となりましたが、やっぱりメモリ使用量に関する罠があったので、苦労はありました(´・ω・｀)
* それでも試行錯誤していくうちにメモリが減っていく様子を見るのは楽しかったです(´ω｀)