こんにちは!
情熱開発部プログラム2課の柄本です。
今年は流行り病も落ち着きを見せ、CEDECが現地で聴講できるようになりました!
数量限定でもう完売してしまったようですので、今から現地パス取ることはできませんが、オンラインでも見ることができるので楽しみですね。
そんなCEDECでも触れられる(はず)な、Unityの最新機能について調査をしてきました。
今回はUnityから正式リリースされたばかりのECSについて、その中でもECS Graphicsを調べてきましたので、皆さんと共有できればと思います。
ECSとは
ECS
ECS(Entity Component System)とは、UnityがGameObjectに変わる仕組みとして新たに打ち出したシステムです。
https://unity.com/ecs
ECSが適用されたオブジェクトはGameObjectではなくEntityというものとして生成されます。
これにより、GameObjectやMonobehaviourが抱えていたいくつかの問題、
例えば必ずTransform情報が必要であることや、Update処理が重いことなどを解決できるようになります。
今回はECSの中でも描画関連に関わるECS Graphicsがメインテーマとなりますので詳しくは割愛しますが、今までとは考え方やコードの書き方が大きく変わるため早めに勉強しておくと良さそうです。
ECS Graphics
主にECSによって生成されたEntityを描画するための仕組みが含まれております。
https://docs.unity3d.com/Packages/com.unity.entities.graphics@1.0
そのため、ECS Graphics固有の機能を使う使わないに関わらず、ECSとセットで導入することが基本になります。
そのような中でもいくつか新機能として実装されたものがあり、
今回はDOTS Instancingについてどのようなものか説明していきます。
DOTS Instancing
DOTS Instancingとは、描画可能なEntityに対して適用されるGPU Instancingのことです。
以前までのURP・HDRPではSRP BatcherとGPU Instancingの併用ができませんでした。
SRP Batcherを有効にすればSetPass Callsは減りますがGPU Instancingが使えず、逆にGPU Instancingを使えば一部でDraw Callsは減りますがSRP Batcherが使用できなくなります。
なので、今までは基本的にはSRP Batcherを使用し、Draw Callsの削減を狙いたい箇所ではGPU InstancingをONにするという対応が求められました。
ですが、皆さんこう思ったことはないでしょうか。
GPU InstancingとSRP Batcher一緒に使いたい!
と。
今回のDOTS Instancingという機能はSRP BatcherとGPU Instancingの併用を可能にしました。
さらなる描画負荷の低下を行うことができるようになったのはもちろんですが、
こちらではSRP Batcherで、あちらではGPU Instancingでみたいなことを考えなくて良くなったのは作業効率の向上につながると考えます。
次の章からは、新規実装されたDOTS Instancingがどのように動いているかを実際のコードを見ながら説明し、その効果がどれほどのものになるかを検証していきます。
プロジェクト作成
今回は以下の環境で検証していきます。
- Unity 2022.3.1f1
- Package
- Entities 1.0.10
- Entities Graphics 1.0.10
- Universal RP 14.0.8
URPのデフォルトシェーダーがDOTS Instancing適用済みなので、検証にはそちらを使用します。
今回のプロジェクト作成にあたり、Unity公式から出されている以下のサンプルを参考にさせていただきました。
https://github.com/Unity-Technologies/EntityComponentSystemSamples
上記サンプル内にDOTS Instancingを適用したシンプルなシェーダーコードもありますので、気になる方はぜひご覧ください。
ECSの導入
上記に記載したPackageをPackage Managerからインストールしてください。
また、Assembly Definitionを使用する場合は、Assembly Definition Referencesに以下を追加してください。
Package | 説明 |
Unity.Entities | ECSの本体 |
Unity.Entities.Graphics | ECS graphicsの本体 |
Unity.Collections | ECSを扱う上で必要なCollectionが定義されているので必須 |
Unity.Transform | ECS版Transform |
Unity.Mathematics | ECS版Vector構造体やMatrix構造体が使用できる |
Unity.Mathematics.Extensions | RenderBoundsで使用するAABB構造体が定義されている |
Unity.Mathematics.Extensions.Hybrid | Bounds構造体を上記AABB構造体に変換するメソッドが定義されている |
今回は最低限のものだけ設定をしております。
ECSの真価を発揮するために必要なBurstやJobSystem系は含めておりません。
また、MonobehaviourをEntity化するために必要なBakerクラスも今回は省いております。
Entity生成プログラム
まずはEntityを生成するプログラムを書いていきます。
using Unity.Entities;
using UnityEngine;
public class Spawner : MonoBehaviour
{
public int SpawnNum = 100;
public void Start()
{
EntityCreate();
}
private void EntityCreate()
{
var world = World.DefaultGameObjectInjectionWorld;
var entityManager = world.EntityManager;
int digitNum = (int)Mathf.Log10(SpawnNum) + 1;
string digitFormat = $"D{digitNum}";
for (int i = 0; i < SpawnNum; i++)
{
var entity = entityManager.CreateEntity();
entityManager.SetName(entity, string.Format("MeshEntity{0}", i.ToString(digitFormat)));
}
}
}
上記のコードはただEntityを生成するだけのプログラムです。
GameObject風に言えば、ひたすらCreateEmptyをしているのと同義ですね。
基本的にEntityはEntityManagerをとおして、生成・削除・コンポーネントの追加・編集などを行います。
ここから、描画に必要なコンポーネントをアタッチしていきます。
using Unity.Entities;
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Entities.Graphics;
using Unity.Rendering;
using UnityEngine;
public class Spawner : MonoBehaviour
{
public int SpawnNum = 100;
public float SpawnRange = 5f;
public Material Material;
public Mesh[] Meshes;
public void Start()
{
EntityCreate();
}
private void EntityCreate()
{
var world = World.DefaultGameObjectInjectionWorld;
var entityManager = world.EntityManager;
int digitNum = (int)Mathf.Log10(SpawnNum) + 1;
string digitFormat = $"D{digitNum}";
var filterSettings = RenderFilterSettings.Default;
filterSettings.ShadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
filterSettings.ReceiveShadows = false;
var renderMeshArray = new RenderMeshArray(new[] { Material }, Meshes);
var renderMeshDescription = new RenderMeshDescription
{
FilterSettings = filterSettings,
LightProbeUsage = UnityEngine.Rendering.LightProbeUsage.Off,
};
var renderBounds = new RenderBounds[Meshes.Length];
for (int i = 0; i < Meshes.Length; i++)
{
renderBounds[i] = new RenderBounds { Value = Meshes[i].bounds.ToAABB() };
}
for (int i = 0; i < SpawnNum; i++)
{
var entity = entityManager.CreateEntity();
entityManager.SetName(entity, string.Format("MeshEntity{0}", i.ToString(digitFormat)));
var meshIndex = i % Meshes.Length;
RenderMeshUtility.AddComponents(
entity,
entityManager,
renderMeshDescription,
renderMeshArray,
MaterialMeshInfo.FromRenderMeshArrayIndices(0, meshIndex));
var randPos = UnityEngine.Random.insideUnitCircle * SpawnRange;
entityManager.SetComponentData(entity, new LocalToWorld
{
Value = float4x4.TRS(new float3(randPos.x, 0f, randPos.y), quaternion.identity, new float3(0.1f))
});
entityManager.SetComponentData(entity, renderBounds[meshIndex]);
}
}
}
このプログラムで肝となるのが、
RenderMeshUtility.AddComponents()
です。
こちらのメソッドを使用することで、描画に必要なTransform、Mesh、Materialに関連するコンポーネントを一度にアタッチすることができます。
なので、この処理の前に書かれているfilterSettingsやrenderMeshArrayなどは、この処理を動かすために必要な情報をまとめている段階になります。
そして、上記の処理で必要なコンポーネントがアタッチされたので、続くEntityManager.SetCoponentDataで追加情報を設定している形になります。
このコードを実行すると以下のようにオブジェクトが生成されて、Gameビュー上に表示されるようになります。
DOTS Instancingのコード
実はここまでのコードでDOTS Instancingの適用は完了しております。
肝となるのは”RenderMeshArray“と”MaterialMeshInfo“です。
RenderMeshArrayでマテリアルとメッシュを配列で持ち、それをMaterialMeshInfoによって参照することでInstancingを可能としています。
では、なんでInstancingのコードまで書いているかというと、現状この方法でしかメッシュやマテリアルの適用ができないからです。
私が機能を見落とした可能性は否定できませんが、
以前まで使用されていたInstancing無しの描画クラスであるRenderMeshについて以下のような記述があり、サポートがされていないようでした。
// RenderMesh is no longer used at runtime, it is only used during conversion.
// At runtime all entities use RenderMeshArray.
そのため、オブジェクトがEntity化されると自動的にプログラム側はDOTS Instancingの適用条件を満たすということになります。
Shader側は別途条件を満たすために処理を追加する必要があります。
負荷検証
負荷検証にはUnityのProfilerを使用していきます。
検証は以下の条件で比較していきます。
- オブジェクト生成数 : 10000個
- メッシュはCube、Sphere、Cylinder、Capsuleを均等に配置
- 1920 x 1080のFullHD
- GPU:NVIDIA Geforce RTX 2070
- 比較条件
- 負荷対策無し
- SRP Batcherのみ
- DOTS Instancing(SRP Batcher, GPU Instancing併用)
- 使用するマテリアル
- URPのLitシェーダーの_BaseColorをスクリプトでオーバーライドしたもの
- 負荷対策無し・SRPBatcherはMaterial.SetColor
- DOTS InstancingはECSのURPMaterialPropertyBaseColor
まずはSetPass CallsとDraw Callsの比較から。
負荷対策無し | SRP Batcher | DOTS Instancing | |
SetPass Calls | 10.0K | 8 | 2 |
Draw Calls | 10.0K | 10.0K | 5 |
Batches | 10001 | 10001 | 5 |
想定通り、Draw Callsが減りました。
そして、SetPass Callsも減っています。
これはSRP BatcherがメッシュごとにSetPass Callsを行わないといけないのに対して、DOTS Instancingではメッシュとマテリアルを配列として渡すことでSetPass Callsの回数も減っているのだと考えます。
そして、実際の処理時間が以下のようになります。
負荷対策無し | SRP Batcher | DOTS Instancing | |
GPU ms | 55ms | 5ms | 1ms |
負荷対策無しから大幅に下がっているのは当然として、SRP BatcherからDOTS Instancingもかなり処理時間が下がっております。
今回はオブジェクト数10000個に対してスタティックメッシュが4つだけという、かなり極端な状況下での検証なのでわかりやすく差が出ました。
先に記載した通り、現状Entity化したオブジェクトは自動的にGPU Instancingの条件を満たします。
また、Shaderの改造もプロパティの量に左右はされますが、10行程度の追記のみで対応できるので、お手軽に描画負荷の削減に貢献できそうだと感じました。
おまけ
マテリアルプロパティのオーバーライド
検証用のコードでしれっと”URPMaterialPropertyBaseColor“というものを使用していましたが、これはECS下でのみ使用できる新たなマテリアルのプロパティをオーバーライドする手法です。
正確にはECSで使用できる”MaterialPropertyAttribute“を用いて作成されたコンポーネントの一種として、URPMaterialPropertyBaseColorがあります。
今回の検証では、完全に同じマテリアルを使用すると負荷対策無しとSRP Batcherに差異が生まれなかったため、対策として色を変更することにしました。
その際、ECSではMaterial.SetColorが使用できなかったため、ECSの作法に則りURPMaterialPropertyBaseColorで色の変更を行ったということになります。
こちらは若干使用感に癖があるのですが、マテリアルをRendererから複製する必要がなくなるのでメモリリークが起きる可能性を減らすことができます。
今回、参考にさせていただいたSampleに使い方が載っておりますので、こちらも気になる方は確認してみてください。
GameObjectのEntity化
今回は、スクリプトからEntityを作成していきましたが、既存のGameObjectをEntityに変換する方法もあります。
Subsceneというものを作成し、その中にGameObjectを配置するだけで自動的にEntityに変換されます。
TransformやRendererなどのデフォルトで用意されているコンポーネントは自動でECS仕様のコンポーネントに変換されるので、
例えば何もスクリプトがアタッチされていない背景オブジェクトなどはこの手法で簡単にDOTS Instancing対応をすることができます。
ただし、Monobehaviour系の処理は機能しなくなるので、対応していないスクリプトはBakerなどを利用して書き換える必要があります。
最後に
今回は正式リリースされたばかりのECS Graphicsを触っていきました。
完全に新しい仕組みということで、GameObject・Monobehaviourに慣れているとかなり癖があるように感じます。
また、通常のHierarchyではEntityが見えない、EntityはSRP Batcher対応していないと描画されない等、実際に使用してみないとわからない落とし穴のような仕様があります。
色々と慣れるのに時間を要しそうですが、一方で今回紹介したDOTS InstancingやBurst, JobSystemと併用した高速Update処理など、魅力的な機能も多く含まれております。
少しずつ勉強して、理解を深めていきたいですね。
参考サイト
- Unity Entities Package Manual
- Unity Entities Graphics Package Manual
- Unity EntityComponentSystem Samples
【免責事項】
本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。