第4章 Tuning Practice - Asset

ゲーム製作ではテクスチャやメッシュ、アニメーション、サウンドなどさまざまな種類のアセットを大量に扱います。そこで本章ではそれらのアセットに関して、パフォーマンスチューニングを行う上で気をつけるべき設定項目など、実践的な知識についてまとめます。

4.1 Texture

テクスチャの元となる画像データはゲーム製作において欠かせない存在です。その一方でメモリ消費量は比較的多くなるため設定は適切に行う必要があります。

4.1.1 インポート設定

図4.1はUnityにおけるテクスチャのインポート設定です。

テクスチャ設定

図4.1: テクスチャ設定

この中でもとくに注意すべきものについて紹介します。

4.1.2 Read/Write

このオプションはデフォルトでは無効になっています。無効な状態であればGPUメモリにしかテクスチャは展開されません。有効にした場合はGPUメモリだけでなくメインメモリにもコピーされるため、2倍の消費量になってしまいます。そのためTexture.GetPixelTexture.SetPixelといったAPIを使用せずShaderでしかアクセスしない場合、必ず無効にしましょう。

またランタイムで生成したテクスチャは、リスト4.1で示すようにmakeNoLongerReadableをtrueに設定することで、メインメモリへのコピーを回避できます。

リスト4.1: makeNoLongerReadableの設定

 1: texture2D.Apply(updateMipmaps, makeNoLongerReadable: true)

GPUメモリからメインメモリへのテクスチャ転送は時間がかかるため、読み取り可能な場合は、どちらにもテクスチャを展開することでパフォーマンスの向上が図られています。

4.1.3 Generate Mip Maps

Mip Mapの設定を有効にするとテクスチャのメモリ使用量が約1.3倍になります。この設定は3D上のオブジェクトに対して行うことが一般的で、遠くのオブジェクトに対して、ジャギ軽減やテクスチャ転送量削減を目的に設定します。2DスプライトやUI画像に対しては基本的に不要なので、無効にしておきましょう。

4.1.4 Aniso Level

Aniso Levelはオブジェクトを浅い角度で描画した際に、テクスチャの見栄えをボケずに描画するための機能です。この機能は主に地面や床など遠くまで続いているオブジェクトに使用されます。Aniso Level値が高いほどその恩恵を受けられますが、処理コストはかさみます。

Aniso Level適応イメージ

図4.2: Aniso Level適応イメージ

設定値は0~16までありますが少し特殊な仕様となっています。

  • 0: プロジェクト設定によらず必ず無効
  • 1: 基本的には無効。ただし、プロジェクト設定がForced Onの場合は、9~16の値にクランプされる
  • それ以外: その値での設定

テクスチャをインポートするとデフォルトでは値が1になっています。そのためハイスペック端末が対象でない限りForced Onの設定は推奨できません。Forced Onの設定は「Project Settings -> Quality」のAnisotropic Texturesから設定可能です。

Forced Onの設定

図4.3: Forced Onの設定

Aniso Levelの設定が効果のないオブジェクトで有効になっていないか、もしくは効果のあるオブジェクトに対して無闇に高い設定値になっていないかを確認しましょう。

Aniso Levelによる効果は、線形ではなく段階で切り替わっている挙動になっています。筆者が検証した結果では、0~1、2〜3、4~7、8以降という4段階で変化していました。

4.1.5 圧縮設定

テクスチャは特別な理由がない限り圧縮するべきでしょう。もしプロジェクト内に無圧縮なテクスチャが存在した場合、ヒューマンエラーかレギュレーションがない可能性があります。早急に確認しましょう。圧縮設定に関する詳細は「2.3.3 画像の圧縮」にてご確認ください。

このような圧縮設定に関してはヒューマンエラーが起きないようにTextureImporterを利用し自動化することを推奨します。

リスト4.2: TextureImporterの自動化例

 1: using UnityEditor;
 2: 
 3: public class ImporterExample : AssetPostprocessor
 4: {
 5:     private void OnPreprocessTexture()
 6:     {
 7:         var importer = assetImporter as TextureImporter;
 8:         // Read/Writeの設定なども可能
 9:         importer.isReadable = false;
10: 
11:         var settings = new TextureImporterPlatformSettings();
12:         // Android = "Android", PC = "Standalone"を指定
13:         settings.name = "iPhone";
14:         settings.overridden = true;
15:         settings.textureCompression = TextureImporterCompression.Compressed;
16:         // 圧縮形式を指定
17:         settings.format = TextureImporterFormat.ASTC_6x6;
18:         importer.SetPlatformTextureSettings(settings);
19:     }
20: }

またすべてのテクスチャが同じ圧縮形式である必要はありません。たとえばUI画像の中でも全体的にグラデーションがかかっている画像は、圧縮による品質低下が目立ちやすいです。そのような場合は対象となる一部の画像だけ、圧縮率を低めに設定すると良いでしょう。逆に3Dモデルなどのテクスチャは品質低下がわかりにくいため、圧縮率を高くするなど適切な設定を探るのがよいでしょう。

4.2 Mesh

Unityにインポートしたメッシュ(モデル)を扱う場合の注意点を紹介します。インポートしたモデルデータは設定次第でパフォーマンスは上がります。注意すべきポイントは次の4つです。

  • Read/Write Enabled
  • Vertex Compression
  • Mesh Compression
  • Optimize Mesh Data

4.2.1 Read/Write Enabled

Meshの注意点1つ目はRead/Write Enabledです。モデルのインスペクターにあるこのオプションはデフォルトでは無効になっています。

Read/Write設定

図4.4: Read/Write設定

ランタイム中、メッシュにアクセスする必要がなければ無効にしておきましょう。具体的にはモデルをUnity上に配置して、AnimationClipを再生するくらいの使い方であれば、Read/Write Enabledは無効で問題ありません。

有効にすると、CPUでアクセス可能な情報をメモリに保持するためメモリを2倍消費します。無効にするだけでメモリの節約になるためぜひ確認してみてください。

4.2.2 Vertex Compression

Vertex Compressionはメッシュの頂点情報の精度をfloatからhalfに変更するオプションです。これによってランタイム時のメモリ使用量とファイルサイズを小さくすることが可能です。「Project Settings -> Player」のOtherで設定ができ、デフォルト設定では次のようになっています。

Vertex Compressionのデフォルト設定

図4.5: Vertex Compressionのデフォルト設定

ただし、このVertex Compressionは次のような条件に当てはまると無効化されるので気をつけましょう。

  • Read/Writeが有効
  • Mesh Compressionが有効
  • Dynamic Batchingが有効かつ適応可能なメッシュ(頂点数が300以下、頂点属性が900以下)

4.2.3 Mesh Compression

Mesh Compressionはメッシュの圧縮率を変更できます。圧縮率が高いほどファイルサイズが小さくなりストレージの占める割合が削減されます。圧縮されたデータは実行時に展開されます。そのためランタイム時のメモリ使用量には影響がありません

Mesh Compresssionの圧縮設定には4つの選択肢があります。

  • Off: 非圧縮
  • Low: 低圧縮
  • Medium: 中程度の圧縮
  • High: 高圧縮
Mesh Compression

図4.6: Mesh Compression

「4.2.2 Vertex Compression」で触れましたが、このオプションを有効にするとVertex Compressionが無効化されます。とくにメモリ使用量の制限が厳しいプロジェクトでは、このデメリットを把握した上で設定をしてください。

4.2.4 Optimize Mesh Data

Optimize Mesh Dataはメッシュに不要な頂点データを自動で削除する機能です。不要な頂点データの判定には使用しているShaderから自動判定されます。これはランタイム時のメモリ、ストレージともに削減効果があります。「Project Settings -> Player」のOtherで設定が可能です。

Optimize Mesh Dataの設定

図4.7: Optimize Mesh Dataの設定

ただしこのオプションは頂点データが自動削除されるので便利ですが、予期せぬ不具合を引き起こす可能性があるので注意しましょう。たとえばランタイムでMaterialやShaderを切り替えた際、アクセスしたプロパティが削除されており描画結果がおかしくなることがあります。他にもMeshのみをアセットバンドル化する際、Materialの設定が正しくなかった場合に不要な頂点データと判定されることもあります。これはParticle SystemのようなMeshの参照だけを持たせる場合にありがちです。

4.3 Material

Material(マテリアル)はオブジェクトをどのように描画するか決める重要な機能です。身近な機能ですが、使い方を誤ると簡単にメモリリークしてしまいます。本節では安全なマテリアルを使用する方法を紹介します。

パラメーターにアクセスするだけで複製される

マテリアルの最大の注意点は、パラメーターにアクセスするだけで複製されてしまうことです。そして複製されていることに気づきづらいことです。

次のコードを見てみましょう。

リスト4.3: Materialの複製される例

 1: Material material;
 2: 
 3: void Awake()
 4: {
 5:     material = renderer.material;
 6:     material.color = Color.green;
 7: }

マテリアルのcolorプロパティにColor.greenをセットしているだけの簡単な処理です。このrendererのマテリアルは複製されています。そして複製されたオブジェクトは明示的にDestroyする必要があります。

リスト4.4: 複製されたMaterialの削除例

 1: Material material;
 2: 
 3: void Awake()
 4: {
 5:     material = renderer.material;
 6:     material.color = Color.green;
 7: }
 8: 
 9: void OnDestroy()
10: {
11:     if (material != null)
12:     {
13:         Destroy(material)
14:     }
15: }

このように複製されたマテリアルをDestroyすることでメモリリークを回避できます。

生成したマテリアルの掃除を徹底しよう

動的に生成したマテリアルもメモリリークの原因になりやすいです。生成したマテリアルも使い終わったら確実にDestroyしましょう。

次のサンプルコードを見てみましょう。

リスト4.5: 動的に生成したMaterialの削除例

 1: Material material;
 2: 
 3: void Awake()
 4: {
 5:     material = new Material();  // マテリアルを動的生成
 6: }
 7: 
 8: void OnDestroy()
 9: {
10:     if (material != null)
11:     {
12:         Destroy(material);  // 使い終わったマテリアルをDestroy
13:     }
14: }

生成したマテリアルは使い終わったタイミング(OnDestroy)でDestroyしましょう。プロジェクトのルールや仕様に合わせて、適切なタイミングでマテリアルはDestroyしましょう。

4.4 Animation

アニメーションは2D、3D問わず多く使用されるアセットです。本節ではAnimation ClipやAnimatorに関するプラクティスを紹介します。

4.4.1 スキンウェイト数の調整

モーションは内部的にはそれぞれの頂点がどの骨からどれぐらい影響を受けているかを計算して位置を更新しています。この位置計算に加味する骨の数をスキンウェイト数、またはインフルエンス数と呼びます。そのためスキンウェイト数を調整することで負荷削減ができます。ただしスキンウェイト数を減らすと見た目がおかしくなる可能性があるので調整する際には検証しましょう。

スキンウェイト数は「Project Settings -> Quality」のOtherから設定が可能です。

スキンウェイトの調整

図4.8: スキンウェイトの調整

この設定はスクリプトから動的に調整することも可能です。そのため低スペック端末はSkin Weightsを2に設定し、高スペック端末は4に設定するなどの微調整が可能です。

リスト4.6: SkinWeightの設定変更

 1: // QualitySettingsを丸ごと切り替える方法
 2: // 引数の番号はQualitySettingsの並び順で、0始まりです。
 3: QualitySettings.SetQualityLevel(0);
 4: 
 5: // SkinWeightsだけ変更する方法
 6: QualitySettings.skinWeights = SkinWeights.TwoBones;

4.4.2 キーの削減

アニメーションファイルはキーの数に依存してストレージとランタイム時のメモリを圧迫します。キー数を削減する方法の1つとしてAnim. Compressionという機能があります。このオプションはモデルのインポート設定からアニメーションタブを選択することで表示されます。Anim. Compressionを有効にするとアセットインポート時に不要なキーが自動で削除されます。

Anim. Compression設定画面

図4.9: Anim. Compression設定画面

Keyframe Reductionは値の変化が少ない場合にキーが削減されます。具体的には直前の曲線と比較して誤差(Error)範囲内に収まっていた場合に削除されます。この誤差範囲は調整することが可能です。

Errorの設定

図4.10: Errorの設定

少しややこしいですがErrorの設定は項目によって値の単位が違います。Rotationは角度、PositionとScaleがパーセントです。キャプチャした画像の許容誤差はRotationが0.5度、PositionとScaleは0.5%となります。詳しいアルゴリズムはUnityのドキュメント*1にあるので気になる方は覗いてみてください。

Optimalはさらにわかりにくいのですが、Dense Curveというフォーマットと、Keyframe Reductionの2つの削減方法を比較し、データが小さくなる方を採用します。押さえておくべきポイントとしては、DenseはKeyframe Reductionと比べるとサイズは小さくなります。ただしノイジーになりやすいためアニメーションクオリティが低下する可能性があります。この特性を把握した上で、あとは実際のアニメーションを目視して許容できるか確認していきましょう。

4.4.3 更新頻度の削減

Animatorはデフォルト設定では画面に映っていなくても毎フレーム更新を行います。この更新方法を変更できるカリングモードというオプションがあります。

カリングモード

図4.11: カリングモード

それぞれのオプションの意味は次のようになります。

表4.1: カリングモードの説明

種類意味
Always Animate画面外にいても常に更新を行います。(デフォルト設定)
Cull Update Transform画面外にいるときにIKやTransformの書き込みを行ないません。
ステートマシンの更新は行います。
Cull Completely画面外にいるとステートマシンの更新を行ないません。
アニメーションが完全に止まります。

それぞれのオプションについて注意点があります。まずCull Completelyを設定する場合、Rootモーションを利用している際は注意が必要です。たとえば画面外からフレームインするようなアニメーションの場合、画面外にいるためアニメーションは即座に停止されます。その結果いつまでたってもフレームインしなくなります。

次にCull Update Transformです。これはTransformの更新がスキップされるだけなので、とても使い勝手のよいオプションのように感じます。しかし揺れものといったTransformに依存した処理がある場合は注意が必要です。たとえばキャラクターがフレームアウトすると、そのタイミングのポーズから更新がされなくなります。そして再びフレームインした際に新たなポーズに更新されるため、その弾みで揺れものが大きく動く可能性があります。各オプションの一長一短を把握した上で設定を変更するとよいでしょう。

また、これらの設定を用いても動的にアニメーションの更新頻度を細かく変更することはできません。たとえばカメラから距離が離れているオブジェクトのアニメーションの更新頻度を半分にするなどの最適化です。その場合はAnimationClipPlayableを利用するか、Animatorを非アクティブにしたうえで自身でAnimator.Updateを呼ぶ必要があります。どちらも自前でスクリプトを書く必要がありますが、前者に比べ後者の方が簡単に導入できるでしょう。

4.5 Particle System

ゲームエフェクトはゲーム演出に欠かせません。UnityではParticle Systemをよく使います。本章ではパフォーマンスチューニング観点で、Particle Systemの失敗しない使い方、注意点について紹介します。

大事なことは次の2点です。

  • パーティクルの個数を抑える
  • ノイズは重いので注意する

4.5.1 パーティクルの個数を抑える

パーティクルの個数は負荷につながります。Particle SystemはCPUで動作するパーティクル(CPUパーティクル)のため、パーティクルの個数が多ければ多いほどCPU負荷は上がります。基本方針として必要最低限のパーティクル数に設定しましょう。必要に応じて個数を調整してみてください。

パーティクルの個数を制限する方法は2つです。

  • Emissionモジュールの放出個数の制限
  • メインモジュールのMax Particlesで最大放出個数の制限
Emissionモジュールで放出個数の制限

図4.12: Emissionモジュールで放出個数の制限

  • Rate over Time: 毎秒放出する個数
  • Bursts > Count: バーストタイミングで放出する個数

これらの設定を調整して、必要最低限のパーティクル数になるよう設定してください。

Max Particlesで放出個数の制限

図4.13: Max Particlesで放出個数の制限

もう1つの方法はメインモジュールのMax Particlesです。上の例では1000個以上のパーティクルは放出されなくなります。

Sub Emittersにも注意

パーティクルの個数を抑える上で、Sub Emittersモジュールも注意が必要です。

Sub Emittersモジュール

図4.14: Sub Emittersモジュール

Sub Emittersモジュールは特定のタイミング(生成時、寿命が尽きた時など)で任意のParticle Systemを生成します。Sub Emittersの設定によっては、一気にピーク数まで到達してしまうため使用の際には注意しましょう。

4.5.2 ノイズは重いので注意

Noiseモジュール

図4.15: Noiseモジュール

Noise(ノイズ)モジュールのQualityは負荷が上がりやすいです。ノイズは有機的なパーティクルを表現可能で、お手軽にエフェクトのクオリティをあげられるためよく使われます。よく使う機能だからこそパフォーマンスには気をつかいたい所です。

NoiseモジュールのQuality

図4.16: NoiseモジュールのQuality

  • Low(1D)
  • Midium(2D)
  • High(3D)

Qualityは次元が上がるほど負荷は上がります。もしNoiseが不要であればNoiseモジュールをオフにしましょう。また、ノイズを使う必要があれば、Qualityの設定はLowを優先し、要求に応じてQualityをあげていきましょう。

4.6 Audio

サウンドファイルをインポートしたデフォルト状態はパフォーマンス的には改善ポイントがあります。設定項目は次の3点です。

  • Load Type
  • Compression Format
  • Force To Mono

これらをゲーム開発でよく使うBGM、効果音、ボイスで適切な設定にしましょう。

4.6.1 Load Type

サウンドファイル(AudioClip)をロードする方法は3種類あります。

AudioClip LoadType

図4.17: AudioClip LoadType

  • Decompress On Load
  • Compressed In Memory
  • Streaming

Decompress On Load

Decompress On Loadは、非圧縮でメモリにロードします。CPU負荷が低いため待機時間が小さく再生されます。その反面、メモリを多く使用します。

尺が短くすぐに再生してほしい効果音にオススメです。BGMや尺の長いボイスファイルでの使用は、メモリを多く使用してしまうため注意が必要です。

Compressed In Memory

Compressed In Memoryは、AudioClipを圧縮した状態でメモリにロードします。再生するタイミングで展開するということです。つまりCPU負荷が高く、再生遅延が起きやすいです。

ファイルサイズが大きく、メモリにそのまま展開したくないサウンドや、多少の再生遅延に問題ないサウンドが適しています。ボイスで使うことが多いです。

Streaming

Streamingは、その名の通りロードしながら再生する方式です。メモリ使用量は少ない代わりにCPU負荷が高くなります。尺の長いBGMでの使用がオススメです。

表4.2: ロード方法と主な使用用途まとめ

種類用途
Decompress On Load効果音
Compressed In Memoryボイス
StreamingBGM

4.6.2 Compression Format

Compression formatとは、AudioClip自体の圧縮フォーマットです。

AudioClip Compression Format

図4.18: AudioClip Compression Format

PCM

非圧縮で、メモリを大量に消費します。音質によほどクオリティを求めない限り設定することはありません。

ADPCM

PCMに比べてメモリ使用量は70%減りますが、クオリティは低くなります。CPU負荷がVorbisと比較して格段に小さいのが特徴です。つまり展開スピードが速いため、即時再生や大量に再生するサウンドに適しています。具体的には、足音や衝突音、武器などのノイズを多く含む且つ、大量に再生する必要のあるサウンドです。

Vorbis

非可逆圧縮フォーマットのため、PCMよりクオリティは下がりますが、ファイルサイズは小さくなります。唯一Qualityを設定できるため、微調整も可能です。全サウンド(BGM、効果音、ボイス)でもっとも使われる圧縮形式です。

表4.3: 圧縮方法と主な使用用途まとめ

種類用途
PCM使用しない
ADPCM効果音
VorbisBGM、効果音、ボイス

4.6.3 サンプルレートの指定

サンプルレートを指定してクオリティの調節できます。全圧縮フォーマットに対応しています。Sample Rate Settingから3種類の方法を選べます。

Sample Rate Settings

図4.19: Sample Rate Settings

Preserve Sample Rate

デフォルト設定です。元音源のサンプルレートが採用されます。

Optimize Sample Rate

Unity側で解析され、最高周波数の成分に基づいて自動で最適化されます。

Override Sample Rate

元音源のサンプルレートを上書きします。8,000〜192,000Hzまで指定可能です。元の音源より高くしても品質は上がりません。元音源よりサンプルレートを下げたい場合に使用します。

4.6.4 効果音はForce To Monoを設定

Unityはデフォルト状態でステレオ再生しますが、Force To Monoを有効にすることでモノラル再生になります。モノラル再生を強制することで左右それぞれのデータを持たなくて良くなるため、ファイルサイズとメモリサイズは半分になります。

AudioClip Force To Mono

図4.20: AudioClip Force To Mono

効果音はモノラル再生でも問題ないことが多いです。また3Dサウンドもモノラル再生の方が良い場合もあります。検討した上でForce To Monoを有効にするとよいでしょう。パフォーマンスチューニング効果も塵も積もれば山となります。モノラル再生で問題ない場合は積極的にForce To Monoを活用しましょう。

パフォーマンスチューニングとは話がずれますが音声ファイルは無圧縮のものをUnityに取り込みましょう。圧縮済みのものをインポートした場合、Unity側でデコード & 再圧縮を行うので品質の低下が発生します。

4.7 Resources / StreamingAssets

プロジェクトには特別なフォルダーが存在します。とくに次の2つはパフォーマンス観点で注意が必要です。

  • Resourcesフォルダー
  • StreamingAssetsフォルダー

通常Unityは、シーンやマテリアル、スクリプトなどから参照されたオブジェクトのみがビルドに含まれます。

リスト4.7: スクリプトで参照されたオブジェクトの例

 1: // 参照されたオブジェクトはビルドに含まれる
 2: [SerializeField] GameObject sample;

先の特別なフォルダーはルールが違います。格納したファイルはビルドに含まれます。つまり、実際には不要なファイルも格納されていればビルドに含まれ、ビルドサイズの膨張につながります。

問題はプログラムから確認することができないということです。不要なファイルを目視で確認しなければならないので時間がかかります。これらのフォルダーには注意してファイル追加しましょう。

しかし、プロジェクトが進行する中で格納ファイルはどうしても増えていきます。中には使用しなくなった不要なファイルが混入することもあるでしょう。結論として、定期的な格納ファイルの見直しをオススメします。

4.7.1 起動時間を遅くするResourcesフォルダー

Resourcesフォルダーに大量のオブジェクトを格納すると、アプリの起動時間が伸びてしまいます。Resourcesフォルダーは文字列参照でオブジェクトをロードできる昔ながらの便利機能です。

リスト4.8: スクリプトで参照されたオブジェクトの例

 1: var object = Resources.Load("aa/bb/cc/obj");

このようなコードでオブジェクトをロードできます。Resourcesフォルダーに格納しておけば、スクリプトからオブジェクトにアクセスできるため、多用してしまいがちです。しかし、Resourcesフォルダーに詰め込みすぎると前述の通りアプリの起動時間が伸びます。原因はUnity起動時に、全Resourcesフォルダー内の構造を解析し、ルックアップテーブルを作成するからです。できる限りResourcesフォルダーの使用は最小限にした方がよいでしょう。

4.8 ScriptableObject

ScriptableObjectはYAMLのアセットで、テキスト形式としてファイル管理しているプロジェクトが多いと思われます。明示的に[PreferBinarySerialization]Attributeを指定することで保存形式をバイナリ形式に変更できます。おもにデータが大量になるようなアセットの場合、バイナリ形式にすることで書き込み・読み込みのパフォーマンスが向上します。

ただし、当然ながらバイナリ形式の場合マージツールなどでは扱いにくくなります。アセット更新が上書きだけで済む、変更差分をテキストで確認する必要がないようなアセットや、ゲーム開発が完了しデータの変更が行われなくなったアセットについては積極的に[PreferBinarySerialization]を指定するとよいでしょう。

ScriptableObjectを使用するにあたって陥りやすいミスは、クラス名とソースコードのファイル名の不一致です。クラスとファイルは同名にする必要があります。クラス作成時には命名に注意しつつ、.assetファイルが正しくシリアライズされ、バイナリ形式で保存されていることを確認しましょう。

リスト4.9: ScriptableObjectの実装例

 1: /*
 2: * ソースコードのファイル名がScriptableObjectSample.csのとき
 3: */
 4: 
 5: // シリアライズ成功
 6: [PreferBinarySerialization]
 7: public sealed class ScriptableObjectSample : ScriptableObject
 8: {
 9:     ...
10: }
11: 
12: // シリアライズ失敗
13: [PreferBinarySerialization]
14: public sealed class MyScriptableObject : ScriptableObject
15: {
16:     ...
17: }