Material Property BlockでMaterial属性操作を差し替え

Unite 上海2017 ではMaterial property Blockを使用して性能改善の紹介がありましたが、筆者はこれについて特別な研究、テストと検証を行い、実験結果とテストプロジェクトをここで共有します。皆様の開発や性能改善にお役に立てば幸いです。

 

一、オフィシャルドキュメント

Unite 上海2017技術特別セッションでは、Arturo Núñezが《Shader Profiling and Optimization》 での元の講演内容は:

Use MaterialPropertyBlock Is faster to set properties using a MaterialPropertyBlock rather than material.SetFloat(); Material.SetColor();

まず、MaterialPropertyBlockに関するオフィシャルドキュメントを調査してみました。MaterialPropertyBlockはGraphics.DrawMeshとRenderer.SetPropertyBlockこの二つのAPIに使用されます。たくさんの同じMaterialで違う属性のオブジェクトを描く際に使用します。たとえば、すべてのMeshの色を変更できるが、Rendererの状態を変えることはない。

 

それではRendererを見てみましょう。MaterialとSharedMaterialの二つの属性とGetPropertyBlock、SetPropertyBlockの二つの関数が含まれています。この内の二つの属性はMaterialのアクセスと変更に使用され、二つの関数はMaterial Property Blockを設定と取得に使います。

 

ご存知の通り、Shared Materialを操作する際に、SharedMaterial属性を使います。この属性を変更すると、すべてこのMaterialを使用するGameObjectに変化があります。単一なMaterialを変更したい場合は、Material属性を使用必要がありますが、初回Materialを使用する際に、実はMaterialのコピーが生成さます。つまりMaterial(Instance)です。

 

二、実験

はじめに二つ配列を宣言し、一つは操作するMaterialを保存用、もう一つは操作するmaterial Blockを保存用とします。

GameObject[] listObj = null; 
GameObject[] listProp = null;

 

次は配列の長さのPublic VariateとMaterialPropertyBlockを宣言します。

public int objCount = 100; 
MaterialPropertyBlock prop = null;

 

Start関数内で初期化処理を行い、スクリーン左側のスペースでObjCount個の球体Sphereを生成し、Material処理に使用します。スクリーン右側スペースでOBjCount個の球体Sphereを生成し、MaterialPropertyBlock処理に使用します。

void Start () { 
        colorID = Shader.PropertyToID("_Color"); 
        prop = new MaterialPropertyBlock(); 
        var obj = Resources.Load("Perfabs/Sphere") as GameObject;
        listObj = new GameObject[objCount]; 
        listProp = new GameObject[objCount];
        for (int i = 0; i < objCount; ++i)
         {
             int x = Random.Range(-6,-2);
             int y = Random.Range(-4, 4);
             int z = Random.Range(-4, 4);
             GameObject o = Instantiate(obj);
             o.name = i.ToString();
             o.transform.localPosition = new Vector3(x,y,z);
             listObj[i] = o;
         }
         for (int i = 0; i < objCount; ++i)
         {
             int x = Random.Range(2, 6);
             int y = Random.Range(-4, 4);
             int z = Random.Range(-4, 4);
             GameObject o = Instantiate(obj);
             o.name = (objCount + i).ToString();
             o.transform.localPosition = new Vector3(x, y, z);
             listProp[i] = o;
         }
     }

 

次はUpdate関数内で操作連携し、ここは上下キーで操作します。

void Update () {
         if (Input.GetKeyDown(KeyCode.DownArrow))
         {
             Stopwatch sw = new Stopwatch();
             sw.Start();
             for (int i = 0; i < objCount; ++i)
             {
                 float r = Random.Range(0, 1f);
                 float g = Random.Range(0, 1f);
                 float b = Random.Range(0, 1f);
                 listObj[i].GetComponent<Renderer>().material.SetColor("_Color", new Color(r, g, b, 1));
             }
             sw.Stop();
             UnityEngine.Debug.Log(string.Format("material total: {0:F4} ms", (float)sw.ElapsedTicks *1000 / Stopwatch.Frequency));
         }
         if (Input.GetKeyDown(KeyCode.UpArrow))
         {
             Stopwatch sw = new Stopwatch();
             sw.Start();
             for (int i = 0; i < objCount; ++i)
             {
                 float r = Random.Range(0, 1f);
                 float g = Random.Range(0, 1f);
                 float b = Random.Range(0, 1f);
                 listProp[i].GetComponent<Renderer>().GetPropertyBlock(prop);
                 prop.SetColor(colorID, new Color(r, g, b, 1));
                 listProp[i].GetComponent<Renderer>().SetPropertyBlock(prop);
             }
             sw.Stop();
             UnityEngine.Debug.Log(string.Format("MaterialPropertyBlock total: {0:F4} ms", (float)sw.ElapsedTicks * 1000 / Stopwatch.Frequency));
         }
     }

 

もう一度比較データを見てみましょう:

比較結果を見ると、確かにMaterialを使用するより、MaterialPropertyBlockを使用したほうが処理はやいことわかります。処理時間はおおよそMaterialでの処理時間の1/4になります。また、MaterialかMaterial Property Blockに関わらず、初回操作の処理時間が初回以降の処理時間より時間がかかっています。特にmaterialについては、初回Materialを使用して属性変更する際に、コピー処理で非常に時間かかりっています。

 

当然上記のソースコードはまた性能改善できる余地はありますが、毎回Renderer コンポーネントを取得する際にGetComponentの方法で取得していますが、スタートするときにそれを保存すればよいかもしれません。

Renderer[] listRender = null;
Renderer[] listRenderProp = null;
 ...
listRender[i] = o.GetComponent<Renderer>();
 ...
listRenderProp[i] = o.GetComponent<Renderer>();
 ...

 

それではもう一度実行の比較データを見てみましょう。

また、Profilerのmemoryモジュールを通じて、Detailedオプションに切り替えて、サンプリングしてみました。Sence Memory下でMaterialコピーが残っていることがわかりました。(Material操作で残り、MaterialPropertyBlock操作で残らない)これもMaterial操作でインスタンス化があり、MaterialPropertyBlock操作でインスタンス化がないことを検証できました。

 

三、ゲーム内処理

Unityのオフィシャルドキュメントで紹介されたMaterialPropertyBlockと同じように、Unity Terrain System(地形作成システム)もMaterial Property Blockを使用してTreeをレンタリングしています、すべてのTreeは同じMaterialを使用しているが、Treeはそれぞれ違う色、拡大縮小、風Factorを使っています。大きなシーンや空間に対して、きっと動的に地図情報をロードしますが、この場合はGPU Instanceにあわせてさらに性能改善することができます。GPU Instanceを使用する二つのメリットがあります:1、実体オブジェクト自身のCPUコストを低減することが可能;2、DrallCallの作用を低減させること、また、dynamic BatchingのCPUコストやStatic Batchingのメモリコストを低減させることも可能です。しかし残念ながら、Open GL ES 3.0以上のデバイスでしか使用できません。

 

一部のゲーム内でユーザーが自分でキャラクターの肌色を設定できるゲームにとっては、MaterialPropertyBlockのメリットははっきりでてきます。

 

たとえば、同時に100人のプレーヤーがゲーム対戦すれば、もしMaterialで色属性を操作すれば、まず100のMaterialコピーインスタンスを存在させる必要があります。そして、Materialの操作属性自身もMaterial PropertyBlockより少し処理遅いなので、性能改善のゴールは1ミリ秒でもかなり大きく、1ミリ秒ずつの累積がゴールに到達できるかどうかに大きく影響を与えます。

 

四、関連プロジェクト

Arturo Núñez のShader Profiling and OptimizationダウンロードURL:
https://github.com/ArturoNereu/ShaderProfilingAndOptimization

今回のテストプロジェクト:
https://pan.baidu.com/s/1qXPGhTa