Unity:1つのドローコールで多くのヘルスバーを描画します

最近、私は多くのトップビューゲームで非常に一般的な問題を解決する必要がありました。敵のヘルスバーの束を画面上にレンダリングすることです。 このようなもの:









明らかに、できる限り効率的に、できれば1回の描画呼び出しでこれを実行したかったのです。 いつものように、仕事を始める前に、私は他の人の決定について少しオンラインで調査しましたが、結果は非常に異なっていました。



私はコードについて誰も恥ずかしく思いませんが、ソリューションのいくつかは完全に素晴らしいものではなかったと言えば十分です。例えば、誰かが各敵にCanvasオブジェクトを追加しました(これは非常に非効率的です)。



結果として私が出会った方法は、他の人が見たものとは少し異なり、UIクラス(Canvasを含む)をまったく使用していないため、一般に公開することにしました。 そして、ソースコードを学びたい人のために、 Githubに投稿しました



Canvasを使用しないのはなぜですか?



敵ごとに1つのCanvasを選択するのは明らかに悪い決定ですが、すべての敵に共通のCanvasを使用できます。 単一のCanvasは、呼び出しのバッチ処理のレンダリングにもつながります。



ただし、このアプローチに関連する各フレームで実行される作業の量は好きではありません。 Canvasを使用する場合、各フレームで次の操作を実行する必要があります。





とにかく、これはすべてCanvasジオメトリバッファを汚染し、プロセッサ内のすべての頂点データの再構築につながります。 このような単純な要素に対して、これらすべてを実行することは望ましくありませんでした。



私の決定について簡単に



私の仕事のプロセスの簡単な説明:





次に、ソリューションをさらに詳しく見ていきましょう。



インスタンスとは何ですか?



グラフィックスの操作では、標準的な手法が長い間使用されてきました。複数のオブジェクトを結合して、共通の頂点データとマテリアルを持ち、1回の描画呼び出しでレンダリングできるようにします。 すべての描画呼び出しはCPUとGPUに追加の負荷がかかるため、これが必要です。 各オブジェクトに対して単一の描画呼び出しを行う代わりに、それらを一度にレンダリングし、シェーダーを使用して各コピーに可変性を追加します。



これを手動で行うには、メッシュの頂点データを1つのバッファーでX回複製します(Xはレンダリング可能なコピーの最大数です)。その後、シェーダーパラメーターの配列を使用して各コピーを変換/色付け/変更します。 各コピーには、この値を配列インデックスとして使用するための番号付きインスタンスに関する知識を保存する必要があります。 次に、「Nのみにレンダー」という順序のインデックス付きレンダーコールを使用できます。Nは、現在のフレームで実際に必要なインスタンスの数で、Xの最大数よりも少ない数です。



最近のほとんどのAPIには既にこのためのコードがあるため、これを手動で行う必要はありません。 この操作は「インスタンス化」と呼ばれます。 実際、事前定義された制限を使用して、上記のプロセスを自動化します。



Unityエンジンは、インスタンス化もサポートします。独自のAPIと、実装に役立つシェーダーマクロのセットがあります。 たとえば、各インスタンスが完全な3D変換を必要とするなど、特定の仮定を使用します。 厳密に言えば、2Dストリップの場合、完全に必要というわけではありません-単純化を行うことはできますが、単純化が必要なので、それらを使用します。 これにより、シェーダーが簡素化され、円や円弧などの3Dインジケーターを使用できるようになります。



クラス損傷可能



敵にはDamageable



というコンポーネントがあります。これにより、敵にヘルスが与えられ、衝突によるダメージを受けることができます。 この例では、非常に簡単です。



 public class Damageable : MonoBehaviour { public int MaxHealth; public float DamageForceThreshold = 1f; public float DamageForceScale = 5f; public int CurrentHealth { get; private set; } private void Start() { CurrentHealth = MaxHealth; } private void OnCollisionEnter(Collision other) { // Collision would usually be on another component, putting it all here for simplicity float force = other.relativeVelocity.magnitude; if (force > DamageForceThreshold) { CurrentHealth -= (int)((force - DamageForceThreshold) * DamageForceScale); CurrentHealth = Mathf.Max(0, CurrentHealth); } } }
      
      





HealthBarオブジェクト:位置/回転



ヘルスバーオブジェクトは非常に単純です。実際、それは敵に接続されたクワッドです。







このオブジェクトのスケールを使用して、ストリップを長く細くし、敵の上に直接配置します。 その回転を心配する必要はありませんHealthBar.cs



のオブジェクトにアタッチされたコードを使用して修正します。



  private void AlignCamera() { if (mainCamera != null) { var camXform = mainCamera.transform; var forward = transform.position - camXform.position; forward.Normalize(); var up = Vector3.Cross(forward, camXform.right); transform.rotation = Quaternion.LookRotation(forward, up); } }
      
      





このコードは、クワッドを常にカメラに向けます。 シェーダーでサイズ変更と回転を実行できますが、2つの理由でここに実装します。



まず、Unityのインスタンス化では常に各オブジェクトの完全な変換が使用されますが、すべてのデータを転送するため、使用できます。 第二に、ここでスケール/回転を設定すると、ストリップをトリミングするための境界平行四辺形が常に真になります。 サイズと回転のタスクをシェーダーの役割にした場合、Unityは、境界の平行四辺形のサイズと回転がレンダリングするものに対応しないため、画面の端に近いときに表示されるはずのストリップを切り捨てることができます。 もちろん、独自の切り捨て方法を実装することもできますが、通常は、可能であれば、使用しているものを使用することをお勧めします(Unityコードはネイティブであり、私たちよりも多くの空間データにアクセスできます)。



シェーダーを確認した後、ストリップがどのようにレンダリングされるかを説明します。



シェーダーヘルスバー



このバージョンでは、シンプルな古典的な赤緑のストリップを作成します。



左に1つの緑のピクセル、右に1つの赤の2x1テクスチャを使用します。 当然、ミップマッピング、フィルタリング、圧縮をオフにし、アドレス指定モードパラメーターをClampに設定します。つまり、ストリップのピクセルは常に完全に緑または赤になり、エッジの周りには広がりません。 これにより、シェーダーのテクスチャ座標を変更して、赤と緑のピクセルを分離する線を上下に移動できます。



(2色しかないので、シェーダーのstep関数を使用してどちらかのポイントに戻ることができます。ただし、この方法は、必要に応じてより複雑なテクスチャを使用できるため便利です。中央のテクスチャ。)



まず、必要なプロパティを宣言します。



 Shader "UI/HealthBar" { Properties { _MainTex ("Texture", 2D) = "white" {} _Fill ("Fill", float) = 0 }
      
      





_MainTex



は赤緑のテクスチャであり、 _Fill



は0から1までの値です。1は完全なヘルスです。



次に、ストリップをオーバーレイキューでレンダリングするように注文する必要があります。つまり、シーン内のすべての深度を無視し、すべての上にレンダリングします。



  SubShader { Tags { "Queue"="Overlay" } Pass { ZTest Off
      
      





次の部分は、シェーダーコード自体です。 ライティングなしのシェーダーを作成している(点灯していない)ため、さまざまなUnityサーフェスシェーダーとの統合について心配する必要はありません。これは、頂点シェーダー/フラグメントシェーダーのほんの一部です。 まず、ブートストラップを作成します。



  CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc"
      
      





ほとんどの場合、これは標準ブートストラップです。ただし、 #pragma multi_compile_instancing



は例外で、Instancing用にコンパイルするものをUnityコンパイラーに指示します。



頂点構造にはインスタンスデータが含まれている必要があるため、次のことを行います。



  struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID };
      
      





また、Unity(変換)が処理するものに加えて、データインスタンスに含まれる内容を正確に指定する必要があります。



  UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float, _Fill) UNITY_INSTANCING_BUFFER_END(Props)
      
      





したがって、Unityは各インスタンスのデータを保存するために「Props」というバッファーを作成する必要があり、その内部で_Fill



というプロパティのインスタンスごとに1つのfloatを使用することを_Fill



ます。



複数のバッファを使用できます。 異なる周波数で更新された複数のプロパティがある場合、実行する価値があります。 それらを分割すると、たとえば、別のバッファを変更するときに1つのバッファを変更しないことができます。これはより効率的です。 しかし、これは必要ありません。



サイズ、位置、回転は既に変換のために転送されているため、頂点シェーダーはほぼ完全に標準的な作業を行います。 これは、各インスタンスの変換を自動的に使用するUnityObjectToClipPos



を使用して実装されます。 インスタンス化せずに、これは通常、単一のマトリックスプロパティを使用して簡単になると想像できます。 しかし、エンジン内でインスタンス化を使用すると、マトリックスの配列のように見え、Unityはこのインスタンスに適したマトリックスを独立して選択します。



また、 _Fill



プロパティに従って、UVを変更して遷移ポイントの位置を赤から緑に変更する必要があります。 関連するコードスニペットを次に示します。



  UNITY_SETUP_INSTANCE_ID(v); float fill = UNITY_ACCESS_INSTANCED_PROP(Props, _Fill); // generate UVs from fill level (assumed texture is clamped) o.uv = v.uv; o.uv.x += 0.5 - fill;
      
      





UNITY_SETUP_INSTANCE_ID



およびUNITY_ACCESS_INSTANCED_PROP



は、このインスタンスの定数バッファーから正しいバージョンの_Fill



プロパティにアクセスすることにより、すべての魔法を実行します。



通常の状態では、四角形のUV座標がテクスチャインターバル全体をカバーし、ストリップの分割線がテクスチャの水平方向の中央にあることがわかります。 したがって、小さな数学的計算によってストリップが水平方向に左右にシフトされ、テクスチャのクランプ値によって残りの部分が塗りつぶされます。



すべての作業が既に完了しているため、フラグメントシェーダーをよりシンプルにすることはできません。



  return tex2D(_MainTex, i.uv);
      
      





完全なコメントシェーダーコードはGitHubリポジトリで入手できます



ヘルスバー素材



その後、すべてが簡単です-このシェーダーが使用するマテリアルをストリップに割り当てるだけです。 他にほとんど何もする必要はありません。上部で目的のシェーダーを選択し、赤緑のテクスチャを割り当て、そして最も重要なこととして、「GPUインスタンス化を有効にする」ボックスをチェックします



画像



HealthBar塗りつぶしプロパティの更新



ヘルスバーオブジェクト、シェーダー、レンダリングするマテリアルがあります。 _Fill



、各インスタンスに_Fill



プロパティを設定する必要があります。 HealthBar.cs



内で次のようにこれを行います。



  private void UpdateParams() { meshRenderer.GetPropertyBlock(matBlock); matBlock.SetFloat("_Fill", damageable.CurrentHealth / (float)damageable.MaxHealth); meshRenderer.SetPropertyBlock(matBlock); }
      
      





CurrentHealth



クラスのCurrentHealth



0から1の値に変換しCurrentHealth



で除算しMaxHealth



。 次に、 MaterialPropertyBlock



を使用して_Fill



プロパティに_Fill



ます。



インスタンス化せずにMaterialPropertyBlock



を使用してデータをシェーダーに転送していない場合は、学習する必要があります。 Unityのドキュメントでは詳しく説明されていませんが、各オブジェクトからシェーダーにデータを転送する最も効率的な方法です。



この場合、インスタンス化を使用すると、すべてのヘルスバーの値が定数バッファーにパックされるため、一度に転送して描画できます。



ここには、変数を設定するための定型文以外にほとんど何もありません。コードはかなり退屈です。 詳細については、GitHubリポジトリをご覧ください。



デモ



GitHubリポジトリにはテスト用のデモがあります 。このデモでは、記事で説明されているストライプによって表示されるダメージを受けながら、たくさんの邪悪な青いキューブが英雄的な赤い球体(破壊!)によって破壊されます。 Unity 2018.3.6f1で作成されたデモ。



インスタンス化を使用する効果は、次の2つの方法で確認できます。



統計パネル



[再生]をクリックした後、[ゲーム]パネルの上にある[統計]ボタンをクリックします。 ここでは、インスタンス化によって描画呼び出しがいくつ保存されているかを確認できます。



画像



ゲームを起動した後、HealthBarマテリアルをクリックし、「GPUインスタンス化を有効にするチェックボックスをオフにすると、保存された呼び出しの数がゼロに減ります。



フレームデバッガー



ゲームを起動したら、[ウィンドウ]> [分析]> [フレームデバッガー]に移動し、表示されるウィンドウで[有効にする]をクリックします。



左下には、実行されたすべてのレンダリング操作が表示されます。 敵とシェルには多くの個別の課題がありますが(必要に応じて、インスタンス化も実装できます)。 一番下までスクロールすると、「メッシュの描画(インスタンス化)ヘルスバー」という項目が表示されます。



この1回の呼び出しですべてのストリップがレンダリングされます。 この操作をクリックしてからその操作をクリックすると、すべてのストリップが1回の呼び出しで描画されるため、すべてのストリップが消えることがわかります。 フレームデバッガーを使用している場合、マテリアルの[GPUインスタンスを有効にする]チェックボックスをオフにすると、1行が複数行になり、フラグを再度1行に設定したことがわかります。



このシステムを拡張する方法



前述したように、これらのヘルスバーは実際のオブジェクトなので、単純な2Dバーをより複雑なものに変えることを妨げるものは何もありません。 それらは、アークで減少する敵の下の半円、または頭の上で回転する菱形にすることができます。 同じアプローチを使用しても、1回の呼び出しですべてをレンダリングできます。



All Articles