Unity-DOTSで作る2D弾幕シューティングの例の五回目です。
自作描画システムについて
描画に関してですが、私がこのゲームを作り始めた時点ではEntity Component Systemに関連するパッケージの中に従来のSpriteRendererに相当するものが存在しなかったため、絵を表示する機能はGraphics関数を自前で叩く実装をしています。
SpriteRendererっていうのは便利だったんだなあ。
Graphics.DrawMeshがいわゆるドローコールに相当するものですが、これを叩いて描画を行います。
が、せっかくDOTSの恩恵を得て大量のオブジェクトを並列Jobで回しているのに、ドローコールがEntityごとに一回ずつでは速度も何もあったものではないので、このゲームではマテリアルごとにドローコールをバッチングできるGraphics.DrawMeshInstancedのほうを使います。
これによって多少なりとも速度は出るようになりますが、それでもレンダリング処理の負荷はかなり高いです。ここの速度を改善するためにいくつかつつましやかな努力をするわけですが、そのため若干機能が複雑化しています。現状は単一のSystemの中にすべての描画機能がおさまっているので、まだマシかもしれないですが。
ちなみに、スプライトアニメーションを使いたかったため、今回使用するマテリアルにあたるシェーダも自前で用意します。暗黒大車輪? はい。まあ趣味でやるぶんにはいいでしょう。
自前のスプライトアニメーション描画機能に関しては、下記のような手順を踏みます。
- シェーダの作成
- シェーダコードの出力
- マテリアルの作成
- マテリアルの情報をデータ化
- レンダリング用のComponentSytemを作成
シェーダの作成
まずアニメーションさせたい画像を用意します。
この画像をソースとして設定でき、一枚ごとのスプライトのサイズと合計枚数からアニメーションできるようなシェーダを作成します。
下記のような感じです。
ダメージを受けた際に赤く光るティント表現を同じくシェーダ上で作っているのでそれが合流して少し広く見えますが、アニメーションをすること自体はFlipbookノードだけで事足るので便利です。
ShaderGraphはすごい。ちなみにShaderGraphの元になったっぽいShaderForgeの開発者であるFreya Holmérという方が、彼女のYoutubeチャンネルでプログラミングにおける描画や、プログラミングにおける数学に関する動画をいくつも公開していて、かなりオススメです。
ShaderGraphでFlipbookノードを使ったアニメーションを作成する方法としては、下記を参考にしました。
Controlling Flipbook Animation with Shader Graph - unity shader graph tutorial 4
ちなみにスプライトティントのシェーダ実装については下記を参考にしました。
Sprite Tint - 2D Shader Graph Tutorial
シェーダコードの出力
こうして作ったシェーダを使ってマテリアルを作成するわけですが、一つ落とし穴がありました。
私が作っていた時点で、Graphics.DrawMeshInstancedがまだShaderGraphに対応していなかったのです。なのでちょっと一手間加えます。
ShaderGraphのMasterノードからShow Generation Codeを選択し、シェーダコードをコピーします。
UnityEditor上の適当なところに、下記のような感じで新たにシェーダを作成します。
この中に、ShaderGraphからコピーしたコードを貼り付けます。
が、これだけだとまだ動きません。
コピーしてきたシェーダコードのうち、下記の二点を行う必要があります。
- CBUFFERで定義されている変数を、UNITY_INSTANCING_BUFFERに書き換える。
- 変数を使用している箇所を、UNITY_DEFINE_INSTANCED_PROPに書き換える。
CBUFFER_START(UnityPerMaterial)
float _TileNum;
float _TileWidth;
float _TileHeight;
float _Alpha;
float4 _TintColor;
CBUFFER_END
UNITY_INSTANCING_BUFFER_START(Props)
UNITY_DEFINE_INSTANCED_PROP(float, _TileNum)
UNITY_DEFINE_INSTANCED_PROP(float, _TileWidth)
UNITY_DEFINE_INSTANCED_PROP(float, _TileHeight)
UNITY_DEFINE_INSTANCED_PROP(float, _Alpha)
UNITY_DEFINE_INSTANCED_PROP(float4, _TintColor)
UNITY_INSTANCING_BUFFER_END(Props)
これを、変数を使用している箇所すべてに対して行っていきます。
正直なところこれがなぜ必要なのかよくわかっていませんが、まあ動いているしいいんじゃないか。
下記が参考になりました。
Graphics.DrawMeshInstanced Material.SetFloatArray
マテリアルの作成
マテリアルを作成し、さきほど作ったシェーダコードを設定します。
元になるアニメーションのスプライトシートを参考に、横に何枚あるのか、縦に何枚あるのか、というような情報を設定していきます。今のところ絵は正方形のものしか想定していません。
下記のような感じです。
このため、アニメーションが違うものは別マテリアルという扱いで作成していきます。
アニメーションが違うなら、そもそも絵がまるまる違うのだしマテリアルも別でいいだろうという判断なのですが、このあたり実際ほかにいいやり方があるかもしれません。
マテリアルの情報をデータ化
作ったマテリアルを、ゲーム内から読み出せるようにデータ化を行います。
このゲームでは、編集しやすさのためSQLiteにいったんデータを記載してから、それをScriptableObjectに変換したデータテーブルを使います。
このあたりの独自実装についてはこちらで記載しています。
まずSQLiteのテーブルに、作ったマテリアルの一覧を記載します。
これをScriptableObjectに変換したものが下記のようになります。
このScriptableObjectはAddressableとして登録しておきます。
作成したマテリアルのファイルも、Addressableに登録しておきます。下記のような感じです。
ScriptableObjectから、Name
に設定されているマテリアルファイル名を頼りに、マテリアルをロードしていきます。これはゲーム開始時の最初のロード時に行い、読み込んだマテリアルはstaticなDictionaryにデータのIDをキーとして書き込んで、保持しておきます。
下記のような感じです。
// アニメーション情報を読み込む
var animationInfoHandle = Addressables.LoadAssetAsync<SSpriteAnimationInfo>(ScriptableResources.SPRITE_ANIMATION);
yield return new WaitUntil(() => animationInfoHandle.IsDone);
var animationInfoDic = new Dictionary<int, SpriteAnimationInfoStruct>();
foreach (var data in animationInfoHandle.Result.animations)
{
var handle = Addressables.LoadAssetAsync<Material>(materialPrefix + data.name);
yield return new WaitUntil(() => handle.IsDone);
animationMaterialDic.Add(data.id, handle.Result);
animationInfoDic.Add(data.id, data);
}
すぐに使うわけではないマテリアルも全ロードしてしまっているので、ゲームの規模が大きくなってくるとこのあたりは少し問題になりそうです。
ともかく、これでゲーム内からIDを元にマテリアルのデータが引き出せるようになりました。
レンダリング用のComponentSytemを作成
いよいよレンダリング用の実装をしていきます。
まず、レンダリング対象のマーカーとなるIComponentDataを定義しておきます。
public struct RenderSprite : IComponentData
{
public bool isPlayer;
public Matrix4x4 matrix;
public int materialId;
public float speed;
public float count;
public float tileMax;
public float tileNum;
public float tileWidth;
public float tileHeight;
public float cellWidth;
public float cellHeight;
public bool isTinting;
public float4 tintColor;
public int tintTime;
public float2 scale;
public float2 scaleCurrent;
public float2 scaleTarget;
public float scaleTime;
public float scaleCount;
public float alpha;
public float alphaCurrent;
public float alphaTarget;
public float alphaTime;
public float alphaCount;
public int sortingOrder;
public bool isStopLoop;
}
いくつもパラメータがありますがまあそこまで深い意味はありません。sortingOrderなど、レンダリングの際に参照したい値をここに持たせておきます。
本体となるComponentSystemですが、ほぼCode Monkey師匠の下記と同じものです。
100,000 Units Animated in Unity ECS!
いったん下記に記載します。
[UpdateAfter(typeof(ColliderSystem))]
public class SpriteRenderSystem : JobComponentSystem
{
public const int SORTING_ENEMY = 1;
public const int SORTING_EXPLOSION = 1;
public const int SORTING_BULLET = 1;
public const int SORTING_PLAYER = 2;
public const int SORTING_PLAYER_INTERFACE_FILL = 0;
public const int SORTING_PLAYER_INTERFACE_FRAME = 1;
public const int SORTING_PLAYER_OPTION = 1;
public const int SORTING_TIME_BUBBLE = 1;
public const int TINT_TIME_DEFAULT = 3;
public static readonly float4 COLOR_WHITE = new float4(1f, 1f, 1f, 1f);
public static readonly float4 COLOR_RED = new float4(1f, 0f, 0f, 1f);
public static readonly float4 TINT_COLOR_NONE = new float4(1f, 1f, 1f, 1f);
private UtilSystem utilSystem;
private EntityQuery qNeedMaterialSetting;
private EntityQuery qRenderSprite;
private Camera camera;
private Mesh mesh;
private Vector3 one;
private const int POSITION_SLICES = 20;
private NativeQueue<RenderData>[] nativeQueueArray;
private NativeArray<JobHandle> jobHandleArray;
private NativeArray<RenderData>[] nativeArrayArray;
private const int DRAW_MESH_INSTANCED_SLICE_COUNT = 1023;
private EntityManager entityManager;
private struct RenderData {
public Entity entity;
public Matrix4x4 matrix;
public float3 position;
public float tileWidth;
public float tileHeight;
public float tileNum;
public int materialId;
public int sortingOrder;
public float4 tintColor;
public float alpha;
}
protected override void OnCreate()
{
entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
utilSystem = World.GetExistingSystem<UtilSystem>();
one = Vector3.one;
/*
* DrawMeshInstancedで使用するMeshを作成し、キャッシュしておく
*/
mesh = new Mesh();
Vector3[] vertices = new Vector3[4]
{
new Vector3(0, 0, 0),
new Vector3(1, 0, 0),
new Vector3(0, 1, 0),
new Vector3(1, 1, 0)
};
mesh.vertices = vertices;
int[] tris = new int[6]
{
// lower left triangle
0, 2, 1,
// upper right triangle
2, 3, 1
};
mesh.triangles = tris;
Vector3[] normals = new Vector3[4]
{
-Vector3.forward,
-Vector3.forward,
-Vector3.forward,
-Vector3.forward
};
mesh.normals = normals;
Vector2[] uv = new Vector2[4]
{
new Vector2(0, 0),
new Vector2(1, 0),
new Vector2(0, 1),
new Vector2(1, 1)
};
mesh.uv = uv;
// Camera.mainをキャッシュするが
// FIXME 途中でカメラが変更になって壊れるケースがないか?
camera = Camera.main;
/*
* EntityQueryを作成し、キャッシュする
*/
qRenderSprite = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[] { typeof(RenderSprite), typeof(Translation) },
});
qNeedMaterialSetting = GetEntityQuery(new EntityQueryDesc()
{
All = new ComponentType[] { typeof(RenderSprite), typeof(NeedMaterialSetting) },
});
qNeedMaterialSetting.SetChangedVersionFilter(typeof(NeedMaterialSetting));
/*
* レンダリング対象のソートを高速化するため、その際に使用するNativeQueueの配列を作成しておく
*/
nativeQueueArray = new NativeQueue<RenderData>[POSITION_SLICES];
for (int i = 0; i < POSITION_SLICES; i++)
{
NativeQueue<RenderData> nativeQueue = new NativeQueue<RenderData>(Allocator.Persistent);
nativeQueueArray[i] = nativeQueue;
}
jobHandleArray = new NativeArray<JobHandle>(POSITION_SLICES, Allocator.Persistent);
nativeArrayArray = new NativeArray<RenderData>[POSITION_SLICES];
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
/*
* フレームレートからアニメーション速度を計算し、描画するスプライトフレームを決定する
* ティント、アルファ、スケールの漸次処理を進行させる
* GPUに渡される、RenderSpriteが保持するMatrix4x4にデータを設定する
*/
var FrameJob = new SpriteRender_ControlFrameJob {
FPS = InsanusApplication.FPS,
canProceed = utilSystem.CanProceedFrame(),
isPause = utilSystem.IsPause(),
};
FrameJob.Schedule(qRenderSprite, inputDeps).Complete();
/*
* マテリアルの初期設定が終わっていないものがある場合、設定する
*/
var spriteRender_NeedMaterialSetting_array = qNeedMaterialSetting.ToComponentDataArray<RenderSprite>(Allocator.TempJob);
var needmaterialSetting_array = qNeedMaterialSetting.ToComponentDataArray<NeedMaterialSetting>(Allocator.TempJob);
var entity_NeedMaterialSetting_array = qNeedMaterialSetting.ToEntityArray(Allocator.TempJob);
for (int i = 0; i < spriteRender_NeedMaterialSetting_array.Length; i++) {
if (needmaterialSetting_array[i].Is) {
RenderSprite renderSprite = spriteRender_NeedMaterialSetting_array[i];
var animInfo = SettingManager.AnimationInfoDic[renderSprite.materialId];
renderSprite.tileWidth = animInfo.tileWidth;
renderSprite.tileHeight = animInfo.tileHeight;
renderSprite.tileMax = animInfo.tileMax;
renderSprite.tileNum = 1;
renderSprite.cellHeight = animInfo.cellHeight;
renderSprite.cellWidth = animInfo.cellWidth;
renderSprite.tintColor = TINT_COLOR_NONE;
renderSprite.alphaCurrent = 1f;
renderSprite.alphaTarget = 1f;
renderSprite.alpha = 1f;
renderSprite.scale = new float2(1f, 1f);
renderSprite.scaleCurrent = new float2(1f, 1f);
renderSprite.scaleTarget = new float2(1f, 1f);
entityManager.SetComponentData(entity_NeedMaterialSetting_array[i], renderSprite);
NeedMaterialSetting materialSetting = needmaterialSetting_array[i];
materialSetting.Is = false;
needmaterialSetting_array[i] = materialSetting;
}
}
spriteRender_NeedMaterialSetting_array.Dispose();
needmaterialSetting_array.Dispose();
entity_NeedMaterialSetting_array.Dispose();
/*
* NativeQueueの配列をクリアしておく
*/
for (int i = 0; i < POSITION_SLICES; i++)
{
SpriteRender_ClearQueueJob clearQueueJob = new SpriteRender_ClearQueueJob
{
nativeQueue = nativeQueueArray[i]
};
jobHandleArray[i] = clearQueueJob.Schedule();
}
JobHandle.CompleteAll(jobHandleArray);
/*
* ソート処理の高速化のため、Y座標を元に画面を分割し、
* それぞれの範囲内にあるEntityを参照するnativeQueueの配列を作成する
*/
float3 cameraPosition = camera.transform.position;
float cameraSliceSize = camera.orthographicSize * 2f / POSITION_SLICES;
float sort_1 = cameraPosition.y + camera.orthographicSize; // Top most cull position
float sort_2 = sort_1 - cameraSliceSize * 1f;
float sort_3 = sort_1 - cameraSliceSize * 2f;
float sort_4 = sort_1 - cameraSliceSize * 3f;
float sort_5 = sort_1 - cameraSliceSize * 4f;
float sort_6 = sort_1 - cameraSliceSize * 5f;
float sort_7 = sort_1 - cameraSliceSize * 6f;
float sort_8 = sort_1 - cameraSliceSize * 7f;
float sort_9 = sort_1 - cameraSliceSize * 8f;
float sort_10 = sort_1 - cameraSliceSize * 9f;
float sort_11 = sort_1 - cameraSliceSize * 10f;
float sort_12 = sort_1 - cameraSliceSize * 11f;
float sort_13 = sort_1 - cameraSliceSize * 12f;
float sort_14 = sort_1 - cameraSliceSize * 13f;
float sort_15 = sort_1 - cameraSliceSize * 14f;
float sort_16 = sort_1 - cameraSliceSize * 15f;
float sort_17 = sort_1 - cameraSliceSize * 16f;
float sort_18 = sort_1 - cameraSliceSize * 17f;
float sort_19 = sort_1 - cameraSliceSize * 18f;
float sort_20 = sort_1 - cameraSliceSize * 19f;
SpriteRender_SeparateDataJob separateDataJob = new SpriteRender_SeparateDataJob
{
sort_1 = sort_1,
sort_2 = sort_2,
sort_3 = sort_3,
sort_4 = sort_4,
sort_5 = sort_5,
sort_6 = sort_6,
sort_7 = sort_7,
sort_8 = sort_8,
sort_9 = sort_9,
sort_10 = sort_10,
sort_11 = sort_11,
sort_12 = sort_12,
sort_13 = sort_13,
sort_14 = sort_14,
sort_15 = sort_15,
sort_16 = sort_16,
sort_17 = sort_17,
sort_18 = sort_18,
sort_19 = sort_19,
sort_20 = sort_20,
nativeQueue_1 = nativeQueueArray[0].AsParallelWriter(),
nativeQueue_2 = nativeQueueArray[1].AsParallelWriter(),
nativeQueue_3 = nativeQueueArray[2].AsParallelWriter(),
nativeQueue_4 = nativeQueueArray[3].AsParallelWriter(),
nativeQueue_5 = nativeQueueArray[4].AsParallelWriter(),
nativeQueue_6 = nativeQueueArray[5].AsParallelWriter(),
nativeQueue_7 = nativeQueueArray[6].AsParallelWriter(),
nativeQueue_8 = nativeQueueArray[7].AsParallelWriter(),
nativeQueue_9 = nativeQueueArray[8].AsParallelWriter(),
nativeQueue_10 = nativeQueueArray[9].AsParallelWriter(),
nativeQueue_11 = nativeQueueArray[10].AsParallelWriter(),
nativeQueue_12 = nativeQueueArray[11].AsParallelWriter(),
nativeQueue_13 = nativeQueueArray[12].AsParallelWriter(),
nativeQueue_14 = nativeQueueArray[13].AsParallelWriter(),
nativeQueue_15 = nativeQueueArray[14].AsParallelWriter(),
nativeQueue_16 = nativeQueueArray[15].AsParallelWriter(),
nativeQueue_17 = nativeQueueArray[16].AsParallelWriter(),
nativeQueue_18 = nativeQueueArray[17].AsParallelWriter(),
nativeQueue_19 = nativeQueueArray[18].AsParallelWriter(),
nativeQueue_20 = nativeQueueArray[19].AsParallelWriter(),
};
var separateDataJobHandle = separateDataJob.Schedule(this);
separateDataJobHandle.Complete();
/*
* 描画対象の総数をカウントする
*/
int visibleEntityTotal = 0;
for (int i = 0; i < POSITION_SLICES; i++)
{
visibleEntityTotal += nativeQueueArray[i].Count;
}
/*
* 分割した配列をソートするため、NativeQueueをNativeArrayに変換する
*/
for (int i = 0; i < POSITION_SLICES; i++)
{
NativeArray<RenderData> nativeArray = new NativeArray<RenderData>(nativeQueueArray[i].Count, Allocator.TempJob);
nativeArrayArray[i] = nativeArray;
}
for (int i = 0; i < POSITION_SLICES; i++)
{
SpriteRender_NativeQueueToArrayJob nativeQueueToArrayJob = new SpriteRender_NativeQueueToArrayJob
{
nativeQueue = nativeQueueArray[i],
nativeArray = nativeArrayArray[i],
};
jobHandleArray[i] = nativeQueueToArrayJob.Schedule();
}
JobHandle.CompleteAll(jobHandleArray);
/*
* マテリアルIDごとに、Y座標の高い順に配列内をソートして、描画の整合性を保つ
*/
for (int i = 0; i < POSITION_SLICES; i++)
{
SpriteRender_SortRenderDataJob sortRenderDataJob = new SpriteRender_SortRenderDataJob
{
sortArray = nativeArrayArray[i],
};
jobHandleArray[i] = sortRenderDataJob.Schedule();
}
JobHandle.CompleteAll(jobHandleArray);
/*
* 配列を統合して一つにする
*/
NativeArray<Matrix4x4> matrixArray = new NativeArray<Matrix4x4>(visibleEntityTotal, Allocator.TempJob);
NativeArray<float> tileNumArray = new NativeArray<float>(visibleEntityTotal, Allocator.TempJob);
NativeArray<float> tileWidthArray = new NativeArray<float>(visibleEntityTotal, Allocator.TempJob);
NativeArray<float> tileHeightArray = new NativeArray<float>(visibleEntityTotal, Allocator.TempJob);
NativeArray<float> alphaArray = new NativeArray<float>(visibleEntityTotal, Allocator.TempJob);
NativeArray<float4> tintColorArray = new NativeArray<float4>(visibleEntityTotal, Allocator.TempJob);
NativeArray<int> materialIdArray = new NativeArray<int>(visibleEntityTotal, Allocator.TempJob);
int startingIndex = 0;
for (int i = 0; i < POSITION_SLICES; i++)
{
SpriteRender_MergeArrayJob mergeArrayJob = new SpriteRender_MergeArrayJob
{
nativeArray = nativeArrayArray[i],
matrixArray = matrixArray,
startingIndex = startingIndex,
tileNumArray = tileNumArray,
tileWidthArray = tileWidthArray,
tileHeightArray = tileHeightArray,
materialIdArray = materialIdArray,
tintColorArray = tintColorArray,
alphaArray = alphaArray,
};
startingIndex += nativeArrayArray[i].Length;
jobHandleArray[i] = mergeArrayJob.Schedule(nativeArrayArray[i].Length, 10);
}
JobHandle.CompleteAll(jobHandleArray);
/*
* 統合が済んだので、統合前の分割された配列は捨てておく
*/
for (int i = 0; i < POSITION_SLICES; i++)
{
nativeArrayArray[i].Dispose();
}
/*
* DrawMeshInstancedの最大スライス数の範囲内で、同じマテリアルのものをバッチングして描画する
*/
int batch = 0;
int slice = 0;
for (int i = 0; i < matrixArray.Length; i++) {
batch++;
if (i < matrixArray.Length - 1 &&
i < DRAW_MESH_INSTANCED_SLICE_COUNT &&
materialIdArray[i + 1] == materialIdArray[i]) {
continue;
}
// 自分が末尾である、または次のデータのマテリアルが自分と異なる、またはスライス数の限界に達した
Matrix4x4[] matrixArray_forGpu = new Matrix4x4[batch];
float[] tileNumArray_forGpu = new float[batch];
float[] tileWidthArray_forGpu = new float[batch];
float[] tileHeightArray_forGpu = new float[batch];
float[] alphaArray_forGpu = new float[batch];
float4[] tintColorArray_forGpu = new float4[batch];
int[] materialArray_forGpu = new int[batch];
NativeArray<Matrix4x4>.Copy(matrixArray, slice, matrixArray_forGpu, 0, batch);
NativeArray<float>.Copy(tileNumArray, slice, tileNumArray_forGpu, 0, batch);
NativeArray<float>.Copy(tileWidthArray, slice, tileWidthArray_forGpu, 0, batch);
NativeArray<float>.Copy(tileHeightArray, slice, tileHeightArray_forGpu, 0, batch);
NativeArray<float4>.Copy(tintColorArray, slice, tintColorArray_forGpu, 0, batch);
NativeArray<float>.Copy(alphaArray, slice, alphaArray_forGpu, 0, batch);
NativeArray<int>.Copy(materialIdArray, slice, materialArray_forGpu, 0, batch);
List<Vector4> tintColorList = new List<Vector4>();
for (int j = 0; j < tintColorArray_forGpu.Length; j++)
{
tintColorList.Add(tintColorArray_forGpu[j]);
}
MaterialPropertyBlock block = new MaterialPropertyBlock();
block.SetFloatArray(ShaderProperties.p_TileNum, tileNumArray_forGpu);
block.SetFloatArray(ShaderProperties.p_TileWidth, tileWidthArray_forGpu);
block.SetFloatArray(ShaderProperties.p_TileHeight, tileHeightArray_forGpu);
block.SetVectorArray(ShaderProperties.p_TintColor, tintColorList);
block.SetFloatArray(ShaderProperties.p_Alpha, alphaArray_forGpu);
Graphics.DrawMeshInstanced(
mesh,
0,
SettingManager.AnimationMaterialDic[materialIdArray[i]],
matrixArray_forGpu,
batch,
block);
slice += batch;
batch = 0;
}
matrixArray.Dispose();
tileNumArray.Dispose();
tileWidthArray.Dispose();
tileHeightArray.Dispose();
materialIdArray.Dispose();
alphaArray.Dispose();
tintColorArray.Dispose();
return inputDeps;
}
protected override void OnDestroy()
{
base.OnDestroy();
for (int i = 0; i < POSITION_SLICES; i++)
{
nativeQueueArray[i].Dispose();
}
jobHandleArray.Dispose();
}
[BurstCompile]
private struct SpriteRender_ControlFrameJob : IJobForEach<Translation, RenderSprite>
{
public float FPS;
public bool canProceed;
public bool isPause;
public void Execute(ref Translation translation, ref RenderSprite render)
{
// フレームプロパティの設定
// 1フレーム1枚を最大枚数として表示を制御する
if (!isPause &&
(canProceed || render.isPlayer)) {
render.count += render.speed;
var progress = render.count / FPS;
if (progress > 1.0)
{
render.count = 0;
if (render.tileNum < render.tileMax)
render.tileNum++;
else
render.tileNum = 1;
}
}
// ティントの処理
if (render.isTinting) {
render.tintTime--;
if (render.tintTime == 0)
{
render.tintColor = TINT_COLOR_NONE;
render.isTinting = false;
}
}
// アルファの処理
if (render.alphaTime > render.alphaCount)
{
render.alphaCount++;
var t = render.alphaCount / render.alphaTime;
render.alpha = math.lerp(render.alphaCurrent, render.alphaTarget, t);
}
// スケールの処理
if (render.scaleTime > render.scaleCount)
{
render.scaleCount++;
var t = render.scaleCount / render.scaleTime;
render.scale = math.lerp(render.scaleCurrent, render.scaleTarget, t);
}
var scale = new Vector3(render.cellWidth, render.cellHeight, 0);
scale.x *= render.scale.x;
scale.y *= render.scale.y;
var offsetX = (render.cellWidth * render.scale.x) / 2;
var offsetY = (render.cellHeight * render.scale.y) / 2;
var position = translation.Value;
position.x -= offsetX;
position.y -= offsetY;
position.z = position.y * .01f;
render.matrix = Matrix4x4.TRS(position, Quaternion.identity, scale);
}
}
[BurstCompile]
private struct SpriteRender_ClearQueueJob : IJob
{
public NativeQueue<RenderData> nativeQueue;
public void Execute()
{
nativeQueue.Clear();
}
}
[BurstCompile]
private struct SpriteRender_SeparateDataJob : IJobForEachWithEntity<Translation, RenderSprite>
{
public float sort_1; // Top most cull position
public float sort_2;
public float sort_3;
public float sort_4;
public float sort_5;
public float sort_6;
public float sort_7;
public float sort_8;
public float sort_9;
public float sort_10;
public float sort_11;
public float sort_12;
public float sort_13;
public float sort_14;
public float sort_15;
public float sort_16;
public float sort_17;
public float sort_18;
public float sort_19;
public float sort_20;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_1;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_2;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_3;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_4;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_5;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_6;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_7;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_8;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_9;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_10;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_11;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_12;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_13;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_14;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_15;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_16;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_17;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_18;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_19;
public NativeQueue<RenderData>.ParallelWriter nativeQueue_20;
public void Execute(Entity entity, int index, ref Translation translation, ref RenderSprite render)
{
// RenderDataの作成
// Y座標の値ごとに分割して配列に格納する
// TODO 現在sortingOrderの値をソートに使用していないので、異なるマテリアルごとに描画順が崩れる可能性がある
float posY = translation.Value.y;
RenderData renderData = new RenderData
{
entity = entity,
matrix = render.matrix,
position = translation.Value,
materialId = render.materialId,
sortingOrder = render.sortingOrder,
tileNum = render.tileNum,
tileWidth = render.tileWidth,
tileHeight = render.tileHeight,
tintColor = render.tintColor,
alpha = render.alpha,
};
if (posY < sort_20) { nativeQueue_20.Enqueue(renderData); }
else if (posY < sort_19) { nativeQueue_19.Enqueue(renderData); }
else if (posY < sort_18) { nativeQueue_18.Enqueue(renderData); }
else if (posY < sort_17) { nativeQueue_17.Enqueue(renderData); }
else if (posY < sort_16) { nativeQueue_16.Enqueue(renderData); }
else if (posY < sort_15) { nativeQueue_15.Enqueue(renderData); }
else if (posY < sort_14) { nativeQueue_14.Enqueue(renderData); }
else if (posY < sort_13) { nativeQueue_13.Enqueue(renderData); }
else if (posY < sort_12) { nativeQueue_12.Enqueue(renderData); }
else if (posY < sort_11) { nativeQueue_11.Enqueue(renderData); }
else if (posY < sort_10) { nativeQueue_10.Enqueue(renderData); }
else if (posY < sort_9) { nativeQueue_9.Enqueue(renderData); }
else if (posY < sort_8) { nativeQueue_8.Enqueue(renderData); }
else if (posY < sort_7) { nativeQueue_7.Enqueue(renderData); }
else if (posY < sort_6) { nativeQueue_6.Enqueue(renderData); }
else if (posY < sort_5) { nativeQueue_5.Enqueue(renderData); }
else if (posY < sort_4) { nativeQueue_4.Enqueue(renderData); }
else if (posY < sort_3) { nativeQueue_3.Enqueue(renderData); }
else if (posY < sort_2) { nativeQueue_2.Enqueue(renderData); }
else { nativeQueue_1.Enqueue(renderData); }
}
}
[BurstCompile]
private struct SpriteRender_NativeQueueToArrayJob : IJob {
public NativeQueue<RenderData> nativeQueue;
public NativeArray<RenderData> nativeArray;
public void Execute()
{
int index = 0;
RenderData renderData;
while (nativeQueue.TryDequeue(out renderData)) {
nativeArray[index] = renderData;
index++;
}
}
}
[BurstCompile]
private struct SpriteRender_SortRenderDataJob : IJob
{
public NativeArray<RenderData> sortArray;
public void Execute()
{
// FIXME 高負荷
// マテリアルのID順に並べ替える
int maxMaterialId = 0;
for (int i = 0; i < sortArray.Length; i++)
{
for (int j = i + 1; j < sortArray.Length; j++)
{
if (sortArray[i].materialId < sortArray[j].materialId)
{
RenderData tA = sortArray[i];
sortArray[i] = sortArray[j];
sortArray[j] = tA;
}
}
if (sortArray[i].materialId > maxMaterialId) maxMaterialId = sortArray[i].materialId;
}
// マテリアルIDごとにY値の高い順に並べ替える
for (int m = 0; m <= maxMaterialId; m++) {
for (int i = 0; i < sortArray.Length; i++)
{
if (sortArray[i].materialId == m)
{
for (int j = i + 1; j < sortArray.Length; j++)
{
if (sortArray[j].materialId == m)
{
if (sortArray[i].position.y < sortArray[j].position.y)
{
RenderData tA = sortArray[i];
sortArray[i] = sortArray[j];
sortArray[j] = tA;
}
}
}
}
}
}
}
}
[BurstCompile]
private struct SpriteRender_MergeArrayJob : IJobParallelFor {
[ReadOnly] public NativeArray<RenderData> nativeArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<Matrix4x4> matrixArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<float> tileNumArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<float> tileWidthArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<float> tileHeightArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<int> materialIdArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<float4> tintColorArray;
[NativeDisableContainerSafetyRestriction] public NativeArray<float> alphaArray;
public int startingIndex;
public void Execute(int index)
{
RenderData renderData = nativeArray[index];
matrixArray[startingIndex + index] = renderData.matrix;
tileNumArray[startingIndex + index] = renderData.tileNum;
tileWidthArray[startingIndex + index] = renderData.tileWidth;
tileHeightArray[startingIndex + index] = renderData.tileHeight;
materialIdArray[startingIndex + index] = renderData.materialId;
tintColorArray[startingIndex + index] = renderData.tintColor;
alphaArray[startingIndex + index] = renderData.alpha;
}
}
}
描画がバッチングできるDrawMeshInstancedですが、これにもバッチできる数が1023までという制限があり、これを超えるとエラーになります。
また、自分でDrawMeshInstancedを発行する場合、Unityが裏で行ってくれていたソーティングやカリングの処理をすべてすっ飛ばすことになるので、本来透過してほしい部分が透過されなくなってしまったりということが起こります。
下記のような状態になるわけです。(上記のビデオ[100,000 Units Animated in Unity ECS!]より)
これを回避するために、自前で描画の整合性を保つためにソーティングの処理を行う必要が出てくるのですが、そのソート処理もまた数によってボトルネックになってしまうため、いったん画面をY座標の値で分割してそれぞれの範囲内でソートを行う、というようなことを実現しようとした結果このようなコードになっています。一応動いています。
まだ完璧とは言えない状態ですが、何かの参考になれば幸いです。