Unity ECSに関する小ネタです。
※タイトルにもある通り、こちらで触れるECSについてはPureECSを前提としたものとなります。(Hybridについては触れません)
ECSの描画周りに関する情報は既存の物だと一部として扱われている物こそあれど、そこに特化して纏められている情報はあまり無いように思われました。そこで「ECSを触ってみたものの、これはそもそもどうやって描画するのか?」と言った触り始めの方向けの情報をメモ序に纏めてみたいと思います。
- ※以下、この記事に於ける注意点です。
- この記事中ではECSの基礎的な部分についての説明は致しません。(例 : そもそもEntityComponentSystemとは何か?と言った話など)
- 最後に参考リンクを載せてあるのでそちらを御覧下さい。
- 内容としては手探りな部分も多々あります。その為に確実に正しい実装や考え方かと言われると微妙な部分もあるかもしれないので、あくまで実装の一例程度に留めて頂けると幸いです。。
- ECS自体についてもまだまだPreviewな状態であり、将来的には実装が変わってくる可能性もあります。その点を踏まえて参考にして頂ければと思います。
- この記事中ではECSの基礎的な部分についての説明は致しません。(例 : そもそもEntityComponentSystemとは何か?と言った話など)
サンプルのプロジェクト一式についてはGitHubにアップしてあるので必要に応じてご覧ください。
mao-test-h/ECS-RendererSample
実装/動作環境
- Unity version
- Unity2018.3.0b2
- 依存パッケージ
- com.unity.entities@0.0.12-preview.15
- ※ECSはPreviewの為か、バージョンによっては破壊的な変更が入っているのでバージョン間に置ける互換性が保証されておりません。
- ※こちらの記事では最新版を前提とした内容且つ旧版の説明については割愛するのでご了承下さい。(必要なComponentData等で多少似ている所はありますが..)
- com.unity.entities@0.0.12-preview.15
実装の話
実装については「Default Worldを使うか?/自前でWorldを生成するか?」「自前のTransformSystem/RendererSystemを使うか?」等の要件によって手順の方は変わってきますが、今回纏める内容については**「自前でワールドを生成」「TransformSystemとRendererSystemはデフォルトで用意されているものを使用する」と言う前提で**手順の方を纏めていきます。
大雑把に描画までの全体手順の方を纏めると以下の様になります。
- Worldを生成し、以下のComponentSystemを登録
- EntityManager
- EndFrameTransformSystem
- RenderingSystemBootstrap
- 以下のComponentDataを持つEntityを生成
- [Position] or [Scale] or [Rotation]
- ※TRSへのアクセスはこちら経由で行える。但し今回のサンプルでは初期化時に「ランダムな位置に配置」「ランダムなサイズに変更」をやっているだけとなる。
- MeshInstanceRenderer
- [Position] or [Scale] or [Rotation]
詳細について順に解説していきます。
ソースについては一部の引用となるので、今回の実装コード全体としては以下のScriptを御覧ください。
ComponentSystemについて
先ずは前提となるWorldの運用について、テラシュールブログさんの方に纏められているので以下を御覧ください。
こちらの方でも自作WorldとDefault Worldの違いについて簡単に説明すると、以下の様になります。
-
自作World
- ユーザーが独自に作成して管理する必要のあるWorld。
- 作成したWorldにはComponentSystemが一切登録されていないので、動かすシステムを自前で登録する必要がある。
-
Default World
- ECSが自動で生成してくれるWorld。
- 現在の仕様だと存在するComponentSystemが全て登録される。その為に裏を返すと使用していない不要なSystemも動いてしまう可能性もある。
- Systemの作りによっては意図せぬ挙動により、毎フレームゴミを出してしまうと言った思わぬ副作用を持った物も出てくるかもしれないので注意。。(自作System/公式System問わず)
- 「UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP」と言うシンボルを定義しておくことで生成されないようにすることが可能。
その他、Hybrid時に於ける注意点などは以前書いた記事の方でも解説してあるので、宜しければご覧ください。
自作Worldを使う場合
自作Worldの場合だと作りたての時点ではComponentSystemが何も登録されていないので、先ずは描画に必要な以下のComponentSystemの登録から行います。
-
EntityManager
- Entityの管理クラス。Entityの生成/ComponentDataの設定/削除諸々を行う。
- 描画に限らずEntityを管理するにあたっては、ほぼどんな状況でも必須になるかと思われる。
-
EndFrameTransformSystem
- デフォルトで用意されているTransform周りのComponentSystem。
- [Position][Rotation][Scale]と言うComponentDataを用いることでEntityの移動/回転/拡縮を行える。
- ※後は今回の例では実装していないが親子構造の構築なども可能。
- 挙動としては簡潔に言うと[Position][Rotation][Scale]の値を[LocalToWorld]と言うComponentData(実体としてはfloat4x4の行列)に詰める形の処理となっている。
- ※逆に言うとEntityにLocalToWorldを直接付けた上で変換行列を直に書き換えてやることでTransformSystemを経由せずに動かすことも出来たりする。
- ※補足として、内部で用意されているTransformSystemクラス自体は抽象クラスとなるので、Worldに登録するのは継承したEndFrameTransformSystemクラスを用いる必要がある。
-
RenderingSystemBootstrap
- デフォルトで用意されているPureECSで描画を行うためのComponentSystemである「MeshInstanceRendererSystem」の補助クラス。
- こちらを経由することでComponentSystemに対しカメラの設定などを行ってくれる。
- 例として挙げるとScene上にもオブジェクトが描画されるようになるので、凝ったことをしない限りはこちらを用いれば良いかと思われる。
- 内部的には「Graphics.DrawMesh」「Graphics.DrawMeshInstanced」で表示を行っている。
- 動かす為に必要なComponentDataとしては「MeshInstanceRenderer」と「LocalToWorld」が必要。
- デフォルトで用意されているPureECSで描画を行うためのComponentSystemである「MeshInstanceRendererSystem」の補助クラス。
// DefaultWorldを消す
// - ※「UNITY_DISABLE_AUTOMATIC_SYSTEM_BOOTSTRAP」と言うシンボルを定義する形でも止められるが、サンプルなので敢えてこうしている。
World.DisposeAllWorlds();
// 自作Worldの立ち上げ及び描画に必要なComponentSystemの設定
World.Active = new World("Sample World");
entityManager = World.Active.CreateManager<EntityManager>();
// デフォルトで用意されているTransform周りのComponentSystem
World.Active.CreateManager(typeof(EndFrameTransformSystem));
// デフォルトで用意されているPureECSで描画を行うためのComponentSystemである「MeshInstanceRendererSystem」の補助クラス
World.Active.CreateManager(typeof(RenderingSystemBootstrap));
Default Worldを使う場合
Default Worldの場合だと最初から必要なシステムがWorldに登録されているので、Systemで動かすためのComponentDataを持つEntityを作るだけで動かすことが可能です。
※蛇足かもしれませんが、今回用意したサンプルの方ではSampleSceneにある「Main Camera -> Botstrap -> Is Use Default World」にチェックを付けることでDefault Worldで動作させることが可能です。
ComponentDataについて
上述の項目でも幾つか出てきているComponentDataについてですが、内容の方を纏めて行こうと思います。
- Position、Rotation、Scale
- データの実体としてはPositionとScaleはfloat3を持つデータであり、Rotationはquaternionを持つデータとなります。
- データの意味については名前の通りとなるので詳細については割愛。
/// <summary>
/// If Attached, in local space (relative to parent)
/// If not Attached, in world space.
/// </summary>
[Serializable]
public struct Position : IComponentData
{
public float3 Value;
}
/// <summary>
/// If Attached, in local space (relative to parent)
/// If not Attached, in world space.
/// </summary>
[Serializable]
public struct Rotation : IComponentData
{
public quaternion Value;
}
/// <summary>
/// If Attached, in local space (relative to parent)
/// If not Attached, in world space.
/// </summary>
[Serializable]
public struct Scale : IComponentData
{
public float3 Value;
}
- LocalToWorld
- 実体としてはfloat4x4のモデル変換行列です。
- TransformSystemは上記の「Position/Rotation/Scale」のどれかが存在する場合には自動的にLocalToWorldを追加して結果を詰めてくれます。
- ※その為にTransformSystemを用いる場合にはEntityのアーキタイプにこちらを追加しなくても動きます。
- 逆に自前でTransformSystemに該当するものを作る上でMeshInstanceRendererSystemを用いる場合には、こちらをアーキタイプに登録した上でデータを入れるなどの対応を行う必要が出てくるかと思われます。
/// <summary>
/// If is not previously added, LocalToWorld is added by system if Rotation +/- Position +/- Scale exist.
/// Updated by system.
/// Read-only from external systems.
/// User responsible for removing.
/// </summary>
[Serializable]
public struct LocalToWorld : IComponentData
{
public float4x4 Value;
}
- MeshInstanceRenderer
- 簡単に言うと描画に関する情報です。
- ISharedComponentDataを実装しているので共通データとして扱われます。
- ※ちなみに、こちらに渡すマテリアルとしては以前のバージョンまでは「GPU Instancing」に対応している必要がありましたが、最近のバージョン?にてGPU Instancing非対応時にはGraphics.DrawMeshで描画する様に処理が変更されていたので対応されていなくても表示自体は可能となります。
/// <summary>
/// Render Mesh with Material (must be instanced material) by object to world matrix.
/// Specified by the LocalToWorld associated with Entity.
/// </summary>
[Serializable]
public struct MeshInstanceRenderer : ISharedComponentData
{
public Mesh mesh;
public Material material;
public int subMesh;
public ShadowCastingMode castShadows;
public bool receiveShadows;
}
Entityの生成
以下のコードはEntityの生成周りの処理となります。
これだけ見ると少し長いように見受けられますが、やっている事は単純にEntityのアーキタイプを定義してそれを元にEntityを作成 → 作成したEntityのComponentDataに初期情報(描画情報と座標など)をセットしているだけです。
なお、コメント中にもリンクを載せてありますがEntityの初期化については@pCYSl5EDgoさんの以下の記事を参考にさせて頂きました。
後はデフォルトだと無効にしておりますが、「ENABLE_SCALE」と言うシンボルを定義することでEntityの拡縮を有効にし、サンプルの挙動に「各Entityのサイズをランダムな大きさに変更する」処理を付け加えることが可能となっております。
// テスト用に表示するEntityのアーキタイプ
// - ランダムな位置に表示させるための「Position」
// - 表示データとなる「MeshInstanceRenderer」
//
// ※後はデフォルトだとシンボルで区切って無効状態にしているが、
// 「ENABLE_SCALE」と言うシンボルを定義することでEntityの拡縮を有効にし、
// サンプルの挙動に「各Entityのサイズをランダムな大きさに変更する」処理を付け加えることが可能となっている。
var archetype = entityManager.CreateArchetype(
ComponentType.Create<Position>(),
#if ENABLE_SCALE
ComponentType.Create<Scale>(),
#endif
ComponentType.Create<MeshInstanceRenderer>());
// Entityの生成(各種ComponentData/SharedComponentDataの初期化)
// やっている事としては以下のリンクを参照。
// - https://qiita.com/pCYSl5EDgo/items/18f1827a5b323a7712d7
var entities = new NativeArray<Entity>(_maxObjectNum, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
try
{
entities[0] = entityManager.CreateEntity(archetype);
// MeshInstanceRendererに対するデータの設定
// →この例ではInspectorから登録したデータをそのまま受け渡している。
entityManager.SetSharedComponentData(entities[0], _look);
unsafe
{
var ptr = (Entity*)NativeArrayUnsafeUtility.GetUnsafePtr(entities);
var rest = NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<Entity>(ptr + 1, entities.Length - 1, Allocator.None);
#if ENABLE_UNITY_COLLECTIONS_CHECKS
NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref rest, AtomicSafetyHandle.GetTempUnsafePtrSliceHandle());
#endif
entityManager.Instantiate(entities[0], rest);
}
// 各種ComponentDataの設定
for (int i = 0; i < this._maxObjectNum; ++i)
{
// 移動の例. Calueに任意の座標を与えることで移動可能。
entityManager.SetComponentData(entities[i], new Position { Value = this.GetRandomPosition() });
#if ENABLE_SCALE
// 拡縮の例. Valueに任意のサイズを与えることで拡縮可能。
entityManager.SetComponentData(entities[i], new Scale { Value = UnityRandom.Range(0.1f, 2f) });
#endif
}
}
finally
{
entities.Dispose();
}
これで大まかな設定の方は完了しました。
シーンを実行すると以下のように大量のSphereが表示されるデモが起動するかと思います。
まとめ
補足も踏まえると少しごちゃごちゃしてしまいましたが...以上が表示までの手順となります。
今回は導入を前提とした記事となるために、先ずはデフォルトで用意されているTransformSystem及びMeshInstanceRendererSystemを用いる形での解説から入りました。
ただ、これらデフォルトで用意されている物については既存の仕組みの流用や汎用性を踏まえた処理となっている様に見受けられるので、ゲームの仕様によっては若干冗長且つ無駄なコストが発生してしまう可能性があります。
そのため、これらを用いずにゲーム仕様に合わせた特化型のTransformSystem/RendererSystemを自作していく手法は全然有りのようにも思えました。
幸いにもECSのソースは参照することが可能なので、既存の物をベースに軽量版を作ると言った事も出来るかもしれません。(例えば「描画APIにGraphics.DrawMeshInstancedIndirectを用いてComputeShaderと併用する」「2Dゲームなので計算内容をそれに特化させ3Dの無駄な演算部分を省く」など)
自作Transform/Rendererについて、もし実装する機会があれば纏めていければと思います。
参考/関連サイト
-
初心者向け情報
-
中~上級者向け情報