しゅみぷろ読者になるしゅみぷろプログラミングとかこの広告は、90日以上更新していないブログに表示しています。Unityはじめに今回の記事ではUnite2018で得た有り余るやる気を発散するために、描画の効率化について書いていこうと思います。MaterialPropertyBlockという機能が結構誤解されがちなので、これについても纏めていこうと思います。バッチングを効かせる上で考慮すべきことここで言うバッチングは静的なものではなく、動的に行われるバッチングを指すこととします。バッチングはざっくり説明すると「同じマテリアルを使っているものの描画は出来る限り纏めちゃおうね」っていうやつです。このバッチングはUnityのバッチング条件に該当する場合自動的に行ってくれます。これからバッチングが切れる条件について見ていきます頂点数多いとバッチングされないメッシュ(サブメッシュ)の頂点数がある特定の数を超えるとバッチングが効かなくなります。このある特定の数はShaderによって代わり、非常に面倒なのですが、以下のような仕様になっているそうです(この特定の数は今後変更される可能性があります)。情報を使っていたら300頂点までバッチングが行われるシェーダーが頂点位置、法線、UV0、UV1、Tangentを使っていたら180頂点までバッチングが行われるシェーダーがそれらの情報を使っていない場合は900頂点までバッチングが行われる以下の画像を見てください。オブジェクトを大量に生成したときの負荷を検証している画像ですが、左はCylinderを、右はSphereを大量生成しています。Cylinderの頂点数は88、Sphereの頂点数は515です。この画像の例では、Shaderは一つの法線情報を使っているものを利用しているので、Sphereを描画している方はバッチングが効かず、Batchesの数がものすごいことになっています。Debuggerでも、バッチングが効かない理由が表示されます。一度にバッチングできる頂点数の上限を超えるとバッチングされない一度にバッチングできる頂点数および頂点インデックス数には上限があります。多くのプラットフォームではこの数が64000に制限されます。macOSでは32000に制限されるようです。上記のGifでは同じマテリアルを割り当てたCylinderをバッチ毎に徐々に表示させています。全て同じマテリアルを利用しているので1度に全て描画して欲しいところですが、バッチングには上記の通り上限があるため、数回に分けて描画が行われていることがわかると思います。macOS環境でチェックしたため、頂点インデックス数32000で頭打ちとなっています。Debuggerでバッチングが切れた理由が以下のように表示されます。スケールの設定値によってはバッチングされない公式ではトランスフォームにミラーリングを含む場合は、オブジェクトはバッチされません。一緒にバッチできません。書かれていますが、ちょっとわかりにくいですね。要するに、Scaleに奇数個の負数が指定されているとバッチングが効かなくなるようです。数個だけ負数にした場合は問題なくバッチングされました。ちなみに親オブジェクトのTransformに設定されたScaleも考慮されます。z)のうち奇数個が負数だった場合にバッチングが無効になります。知らず知らずのうちに実は結構やらかしているタイプのやつですね。Debuggerで出てくるバッチングが効かなかった理由の表示が結構賢いです。GPUInstancingが可能な環境の場合と表示されました。Instancingしてくれれば解決するよと。EmulationでGPUInstancingが利用できない環境にエミュレートした場合と表示されます。Scaleに奇数個負数が入力されているよと。callsも増えるので、普段から気をつけている方も多いと思います。既存のマテリアルをコピーするなどしてインスタンスを作成してRendererに対して割り当てた場合、たとえ同じシェーダー、同じプロパティであろうとも、「別のマテリアルを使っている」と判断されてしまい、バッチングが効かなくなります。マテリアルをコピーして使うことが必要であるケースであれば、これは仕方がないことなのでまぁ良いのですが、問題はRenderer.materialプロパティの存在で、初学者にとってはかなり罠に近い仕様になっていることです。Renderer.materialプロパティにアクセスしてマテリアルの変更を行うと、新しいマテリアルのインスタンスが作成されます。Licenseに従い、本家のコードを参考に簡略化して書いています)。GetMaterialとSetMaterialはネイティブ側で実装されていて、それぞれマテリアルのコピーを行います。マテリアルのコピーを行わずに参照を変更するには、Renderer.sharedMaterialを使用する必要があります。Shaderがバッチングに対応していない場合はバッチングされないShaderによってはバッチングを阻害するものがあります。}が指定されていればバッチングが無効になる、という意味です。バッチングされるとすべてのジオメトリがWorldSpaceに変換されるため、ModelSpaceでの座標系がShader内で利用できなくなります。それを防ぐために、ShaderLab内でバッチングが出来ないことを明示するタグがDisabledBatchingです。これらのタグはShader内でModelSpaceでの頂点変形を行う必要があるShaderで利用されます。2つ目の項目は、Shaderがマルチパスレンダリングを行う場合です。複数のPassが存在する場合、バッチングは無効化されます。カスタムシェーダーではここらへんも考慮して、出来るのであればなるべくシングルパスが理想です。ここで頭に入れておいてほしいのは、「Unityが標準で提供するレンダリングパスを利用している場合、多くの場合バッチングを有効化出来る」ということです。Unity標準のShadowMapを利用したShadowCaster及びShadowReceiverパスはUnityが提供するShadowMap生成及び参照のためのパスであり、このパスを使う場合、自作であろうがなかろうがバッチングを有効にすることが出来ます(もちろんその他のバッチング条件に該当すればの話です)。その他にも、Unityが提供する様々なパスではバッチングが有効になります。callsを増やさないためにはどうしたら良いかについて見ていきます。callsはマテリアルのデータをCPUからGPUに受け渡す際に呼ばれるSetPass命令の呼び出し回数です。これが結構CPU側でボトルネックとなります。callsは悪だ!なるべく潰していけ!」といろんな記事やら資料やらで書かれているため、これについては多くの人が知っている内容かもしれません。callsを減らすために必要な一番基本的な事は、利用するマテリアルのインスタンスを減らすことです。callsを抑える手法についてざっくり見ていきます。callsが絶対に減るというわけではありません。callsが増えてしまう場合もあります。詳しくは「RenderQueueによるドローコルの変化」で書きます。同じような描画を行うマテリアルをなるべく1つに纏めるレンダリング対象に「Shaderもマテリアルのプロパティも同じ値なのに、マテリアルのインスタンスだけ違う」ようなものがある場合は迷わず1つのマテリアルを使いまわすようにしましょう。マテリアルを分けておいて、何かアクションがあった時にプロパティを変更したいのであれば、MaterialPropertyBlockの利用を検討します。似たような処理を行い、一部分だけ処理が違うようなShaderを使っていて、マテリアルのプロパティを同じ値にできるようなものがある場合、Shaderを拡張して1つのShaderでうまいこと描画できないかを考えましょう。処理の分岐のようなものが必要になる場合、それを判断するパラメータはマテリアルのプロパティではなく頂点データに格納してやります。頂点データをうまく活用するマテリアルのプロパティでいろんなパラメータを設定する必要があり、その組み合わせ分マテリアルのインスタンスを増やす必要があるような場合、このパラメータを頂点データに設定することで解決できないかを考えましょう。UnityのStandardShaderを使っていて「赤・緑・青」3種類のマテリアルを作るようなことがあるかもしれません。callsは3になります。Shaderの処理自体は全て同じことをしていて、パラメータが違うだけです。これでは無駄が多いので、それぞれのオブジェクトのメッシュが持つ頂点データに、この色のデータを格納してやります。StandardShaderを改造して、Albedoに格納しておいた色データを乗算して出力するようにします。上記の画像では、Blenderで頂点カラーを赤・緑・青に設定したCubeをそれぞれ出力してUnityにInportし、以下のシェーダーを適用したマテリアルを1つ作って、3つのCubeオブジェクト全てにこのマテリアルを割り当てたときの画像です。このShaderはUnityのStandardSurfaceShaderに入力として頂点カラーを取ってくるようにして、Albedoに乗算しているだけです。callsが1に抑えられていることがわかると思います。同じマテリアルのインスタンスを使用しているためバッチング条件に該当するようになり、Batchesも抑えられています。callsが増えません。この要領で、必要なデータは可能であれば頂点データに入れてあげるようにしましょう。セマンティック」あたりで検索してみてください。応用編になりますが、テクスチャに細かなデータ列を格納してShaderでそれを参照できるように紐付ける情報を設定し、Textureを大量のデータ保管庫として利用することも出来ます。callsちょっとした番外編です。Materialが使われます。callsは増えません(Batchesはちゃんと増えている)。以下の画像では、異なる2枚のテクスチャをuGUIで描画しています。このカラクリは以下の記事で解説されています。BlogCanvasRendererの描画処理(描画前の下準備も含む)はMainThreadから切り離され、別スレッドで並列に実行されていきます。実際にUIを描画していくフェーズになると、用意したUI描画用のバッファに全てのUI要素のレンダリング後、これを合成することで最終的な結果を得ています。以下はProfilerで確認したUI生成処理です。Canvas毎に、各レンダラーの描画についての情報を見ることが出来ます。UI描画用バッファも確認できます。DebuggerではなくProfilerで色々チェックするのが良さそうです。RenderQueueと複数オブジェクトによるドローコールの変化マテリアル毎に割り当てることが出来るQueueに関しての描画順コントロールとパフォーマンスへの影響について考えていきます。レンダリングを行うオブジェクトはマテリアルに割り当てられたQueue毎にソートされ、マテリアル毎に描画されていきます。この描画は、基本的に不透明オブジェクトは手前にあるものから行われていき、透明オブジェクトは奥側から行われます(これらは変更可能です)。不透明オブジェクトが手前から描画されていく理由は、一度描画を行ったピクセルに対しては計算をスキップし、効率よくフラグメントを求めることが出来るからです。透明オブジェクトが奥側から描画されていく理由は、Blendingを正しく行うためです。Unity内ではこの「透明か不透明か」の判断を、Queueが2500(2500以下なら不透明で2501以上なら透明)より大きいかどうかで判断しているっぽいです。因みにQueueには特定の値を表す文字列があり、ではBackgroundは1000、Geometryは2000、AlphaTestは2450、Transparentは3000、Overlayは4000を表します。上記のGifでは、マテリアルのQueueが2500以下なら手前から、2500より大きければ奥から描画されている事が確認できます。誤解の無いよう補足しておくと、Queueが2500以下でもアルファをつけることは可能です。レンダリングが手前から行われるので、深度テストを有効にしていると奥の不透明オブジェクトが描画されなくなります(それはそれで綺麗な半透明なオブジェクトの並びを表現できるので結構使うこともあります)。試しに以下のように深度テストとBlendingを記述した適当なShaderを使って半透明Shaderのテストをしてみます。}}上の画像はQueueを2500より大きな値に設定してレンダリングを行った結果です。透明なオブジェクトは奥側から描画されるため、このようになります。こちらの画像はQueueを2500以下の値でレンダリングを行った結果です。手前の半透明のみが描画され、綺麗な透過に見えます。オブジェクトを複数描画するような場合に注意しなければならないことがあります。異なるマテリアル(Queueの値は同じ)のインスタンスを持つオブジェクトを複数配置する場合です。以下のGifを見てください。StandardShaderを割り当てた2つの色違いのマテリアルを作り、それぞれを適用したオブジェクトを交互に並べています。calls及びBatchesが増えているのがわかると思います。calls及びBatchesが意図した値まで下がっています。透明なオブジェクトの場合、より注意する必要があります。calls及びBatchesに注目してください。先程と比べて数値が跳ね上がったのが確認できると思います。この程度の数なら良いのですが、大量に透明なオブジェクトを描画する場合は細心の注意をはらいましょう。Queueに3000を指定した2つのマテリアルを適当に割り当てて大量のオブジェクトを描画した場合、以下のGifようになります。callsもBatchesもエグいですね。奥側から破綻の無いようBlendingを行っていく必要があるためこんな事になってしまいます。この負荷が問題となってしまう場合、応急処置としてQueueの値を変えることで対応できます(そのかわり意図したようにBlendingが行われなくなります)。以下の画像では、片方のマテリアルのQueueを適当に3001として値をずらしています。MaterialPropertyBlockそもそもMaterialPropertyBlockってなんでしょう。どんなときに役立つのでしょうか。昔のバージョンのリファレンスに、多少詳しく書いてくれてます(現状最新バージョンのリファレンスだと説明が簡素なので注意)。docs.unity3d.comつまりMaterialPropertyBlockは特定のマテリアルのプロパティのみ変更して使いまわしたいときに利用できる(Materialを複数作る必要がない)Graphics.DrawMeshでメッシュを描画する際のパラメーターとして指定できるRenderer.SetPropertyBlockでレンダラーに設定できるといった性質を持ちます。callsが増えますということです。ユースケースによりますが、MaterialPropertyBlockを扱うことでUnityEditor上で複数のマテリアルを作成する必要が無くなる場合もありますし、スクリプト上からは微妙にプロパティの値が異なるだけのマテリアル管理をスマートに行うことが出来ると考えれば良いでしょう。ここまでの説明で分かる通り、MaterialPropertyBlockはレンダリングの効率化を行う銀の弾丸ではありません。複数のRenderer(もしくは描画命令)間でマテリアルをなるべく共通化するための機能だと考えるべきです(Renderer単位でプロパティのオーバーライドを保持できるのでメモリ効率は良くなります)。callsが減らせてCPU負荷を減らせる」というわけではないので注意してください。callsは減らせないし、バッチング条件を満たせなかったらバッチングも行われません。以下はTransparentな1つのマテリアルに対して、ちょっと透明な赤と青のカラーをMaterialPripertyBlockで渡してやる例です。callsもBatchesも700を超えてしまいます。「RenderQueueと複数オブジェクトによるドローコールの変化」でやったような応急処置を施せば多少マシにはなりますが、正しくBlendingが行われなくなります。因みにMaterialPropertyBlockについては以下の記事にとても詳細な解説が載っているので、使い方等は以下を参考にするといいです。batching.といった説明ですが、おそらく間違いです。上記記事内ではSphereを大量に描画していますが、Unity標準のSphereの場合頂点数が多く、バッチングが効かないので、それを勘違いしたのだと思われます。MaterialPropertyBlockでもバッチングは有効になりますので安心してください。最後に何故か最初書こうと思っていたことと全く違うこと書いてました。なんでこんなの書いてるんだろう。)はてなブログをはじめよう!es_programさんは、はてなブログを使っています。ブログを報告する引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。