ボツワナ独立記念日ですので初透光です。
この記事は前回の入門記事(その1)の続きです。
記事更新の間隔をあまりに開けるのもInternet ExplorerみたいなのでGoogle Chromeよろしく細分化して入門記事を投稿するスタイルにします。
1記事1要素ぐらいでいきます。
#前提環境
- Unity2018.3.0b5
- ECS version0.0.12-preview.18
#その2の内容
- Chunk Iteration概説
#Chunk Iterationとは
ECSにおいてEntityやComponentDataを取得して処理する方法は4つあります。
- Chunk Iteration
- おそらく今後暫くの間最も効率的にEntityを取得する方法だと思われます。
- 内部的にChunkの連結リストを一番最初に辿って配列に転写しています。
- そのためChunkに対してランダムアクセスする場合の効率が優れています。
- Component Group
- 簡便かつ行数も少ないです。
- 内部的にはChunkの連結リストを必要に応じて辿っています。
- シーケンシャルアクセスをする限りにおいてはChunk Iterationよりも効率的であると想定できますが、ランダムアクセス性能においては明確に劣ります。
- ComponentDataFromEntity
- 悪くはないですが、キャッシュミスヒットしがちな点が残念です。
- [Inject]記法
- これを選ぶのはありえません。歴史的技術的負債です。
- Unity TechnologiesのECS Teamのレスによると今後このAPIを廃止する予定です。
##Chunk Iterationの利点
「いくつかあるComponentTypeのうち最低1つのComponentTypeを含むEntityを処理したい」
「このComponentTypeを含んでいてもこっちは絶対に含まないでいてほしい」
このような要求に対して以前のECSはきちんと応えられませんでした。
「'Position', 'Rotation'いずれか1つを含むEntityを処理したい。読み取りだけして書き込みはしない。」
上の要求をComponent Groupや[Inject]記法でどう書くかを軽く見てみましょう。
struct GroupPosition
{
public readonly int Length;
[ReadOnly] public ComponentDataArray<Position> Positions;
SubtractiveComponent<Rotation> Rotations;
}
struct GroupRotation
{
public readonly int Length;
SubtractiveComponent<Position> Positions;
[ReadOnly] public ComponentDataArray<Rotation> Rotations;
}
struct GroupPositionRotation
{
public readonly int Length;
[ReadOnly] public ComponentDataArray<Position> Positions;
[ReadOnly] public ComponentDataArray<Rotation> Rotations;
}
[Inject] GroupPosition gPosition;
[Inject] GroupRotation gRotation;
[Inject] GroupPositionRotation gPosRot;
[Inject]記法の無駄な行数は酷いものですね……。
完全に個人的な感想ですがComponentDataArray<T>を入力しようとしてIntelliSenseがComponentArray<T>を候補に出してくるので苦手になりました。
次にComponent Groupを見ましょう。
protected override void OnCreateManager()
{
gPos = GetComponentGroup(ComponentType.ReadOnly<Position>(), ComponentType.Subtractive<Rotation>());
gRot = GetComponentGroup(ComponentType.Subtractive<Position>(), ComponentType.ReadOnly<Rotation>());
gPosRot = GetComponentGroup(ComponentType.ReadOnly<Position>(), ComponentType.ReadOnly<Rotation>());
}
ComponentGroup gPos, gRot, gPosRot;
Component Groupはかなりスッキリしています。
ただ、組み合わせが今回は2つだけでしたので2^2-1=3通りで済み、フィールドも3つでしたが、実際のゲームでは4つとか5つとか酷い時には10以上もComponentTypeを組み合わせねばなりません。
そうなるとComponentGroup型のフィールドだけで1023行とか数えることになり悲惨なことになります。
追記を見てください。スッキリかけるようになりました。
##Chunk Iterationの書き方
前節の例を満たすコードを示しましょう。
EntityArchetypeQuery query = new EntityArchetypeQuery
{
Any = new ComponentType[] { ComponentType.Create<Position>(), ComponentType.Create<Rotation>() },
All = System.Array.Empty<ComponentType>(),
None = System.Array.Empty<ComponentType>(),
};
NativeList<EntityArchetype> foundArchetypeList = new NativeList<EntityArchetype>(Allocator.Persistent);
protected override void OnDestroyManager() => foundArchetypeList.Dispose();
AnyにComponentType[]を与えればいいだけになりました。スケーリングしやすくなりましたね。
EntityArchetypeQuery型というクラスのフィールドを定義しています。
public class EntityArchetypeQuery
{
public ComponentType[] Any;
public ComponentType[] None;
public ComponentType[] All;
}
必ずEntityArchetypeQueryのフィールドに配列を初期化してあげてください。nullのままだとエラー吐きます(preview11時点で確認)。
- Any
- 少なくともいずれか1つのComponentTypeを含む
- None
- ComponentGroupのSubtractive相当
- この配列内のComponentTypeを含まない
- All
- 必ずこの配列内のComponentType全てを含む
ComponentTypeを指定しない場合はそのフィールドにSystem.Array.Empty<ComponentType>()を代入してください。new ComponentType[0]はメモリ的に非効率で非推奨です。
NativeList<EntityArchetype>型のfoundArchetypeListフィールドはOnUpdate()内部で使用します。
EntityArchetypeQueryのクエリを毎フレーム発行し、結果として得られるEntityArchetype(ComponentTypeの配列とChunkのリンクリストを含む構造体)を保存します。
##Chunk IterationのOnUpdate()
実際にChunk Iterationをしてみましょう。
Chunk IterationはOnUpdate内部に書くお呪いがComponent Groupや[Inject]記法より明確に多いのが玉に瑕です。
protected override void OnUpdate()
{
var manager = EntityManager;
manager.AddMatchingArchetypes(query, foundArchetypeList);
ArchetypeChunkComponentType<Position> PositionTypeRO = GetArchetypeChunkComponent<Position>(isReadOnly: true);
var Rotation = GetArchetypeChunkComponent<Rotation>(true);
using(NativeArray<ArchetypeChunk> chunks = manager.CreateArchetypeChunkArray(foundArchetypeList, Allocator.TempJob))
{
for(int i = 0; i < chunks.Length; ++i)
{
NativeArray<Position> positions = chunks[i].GetNativeArray(PositionTypeRO);
if(positions.Length == 0) continue;
NativeArray<Rotation> rotations = chunks[i].GetNativeArray(RotationTypeRO);
for(int j = 0; j < positions.Length; ++j)
Draw(positions[j].Value, rotations[j].Value);
}
}
}
OnUpdate()の冒頭部でfoundArchetypeListをEntityManager.AddMatchingArchetypesメソッドで初期化します。
これはEntityManagerが内部的に抱えるArchetypeのリンクリストを全て辿り、NativeList<EntityArchetype>に加えるメソッドです。
ComponentSystem.GetArchetypeChunkComponent<T>(bool isReadOnly) where T : struct, IComponentDataを使用してArchetypeChunkComponentType<T>を得ています。
このメソッドを使用することでC# Job Systemが適切に依存関係を把握できるようになります。
bool型引数のisReadOnlyがtrueの場合読み取り専用となります。
そしてEntityManager.CreateArchetypeChunkArrayメソッドにEntityArchetypeのリストを渡してNativeArray<ArchetypeChunk>を確保してもらいます。
ArchetypeChunk構造体はChunkを指すポインタを格納しています。
public unsafe struct ArchetypeChunk : IEquatable<ArchetypeChunk>
{
[NativeDisableUnsafePtrRestriction] internal Chunk* m_Chunk;
}
後はChunkをforeachしながら取り出してArchetypeChunk.GetNativeArray<T>(ArchetypeChunkComponentType<T>)メソッドを呼び出すことでComponentDataのNativeArrayを得られます。
このNativeArrayはダイレクトにEntityのComponentDataを表していますので、これに対する書き換えを行うことでComponentDataを操作できます(GetArchetypeChunkComponent<T>(bool isReadOnly)でfalseを渡した場合のみ)。
絶対にGetNativeArrray<T>で得られたNativeArray<T>に対してDisposeをしないでください。
書き換えを行いたい場合は次のようにします。
// var PositionTypeRW = manager.GetArchetypeChunkComponent<Position>(isReadOnly: false);
NativeArray<Position> positions = chunks[i].GetNativeArray(PositionTypeRW);
for(int k = 0; k < positions.Length; ++k)
{
// struct Position : IComponentData { public float3 Value; }
float3 position = positions[k].Value;
position.x += Time.deltaTime * 10;
positions[k] = new Position
{
Value = position,
};
}
unsafeを使用できるのであればより効率的な書き方も可能です。
var positions = chunks[i].GetNativeArray(PositionTypeRW);
unsafe
{
var positionPtr = (Position*)NativeArrayUnsafeUtility.GetUnsafePtr(positions);
for(int k = 0; k < positions.Length; ++k, ++positionPtr)
positionPtr->Value.x += Time.deltaTime * 10;
}
非unsafeコードの場合Position型を丸々代入しています。これはsizeof(float)×3=12byteのコピーが常に発生します。
それに対してunsafeコードは4byteの書き換えですみます。
塵も積もれば山となりますので、日頃からこの程度のunsafeは使いこなしていきましょう。 UnsafeUtilityEx? 使わないのが一番いいです。
##Entityの場合
EntityManager.GetArchetypeChunkEntityType()でArchetypeChunkEntityTypeを入手します。
そしてそれをArchetypeChunk.GetNativeArray(ArchetypeChunkEntityType)に渡してNativeArrray<Entity>を得ましょう。
##SharedComponentDataの場合
その1で説明していない要素もありますが、説明しておきます。
SharedComponentDataはWorldにつき1つのみ存在するSharedComponentDataManagerで一元管理されています。
そしてSharedComponentDataの実際に使用されている(Set/AddSharedComponentDataされた)値に対して0以上のSharedComponentIndexを付与しています。
同一のChunkはSharedComponentDataの組み合わせが同じ物のみを含んでいます。
特別なSharedComponentIndexとして0を必ず覚えましょう。
全てのISharedComponentDataを実装した構造体において必ずdefault値のSharedComponentIndexは0になります。
Chunk Iterationにおいては直接SharedComponentDataをArchetypeChunkから得ることはできません。
EntityManager.GetArchetypeChunkSharedComponentType<T>()でArchetypeChunkSharedComponentType<T>を得、それをint ArchetypeChunk.GetSharedComponentIndex<T>(ArchetypeChunkSharedComponentType<T>)に渡すことでSharedComponentIndexを入手します。
この段階ではChunkが保持するSharedComponentDataがdefault値であるか否かしかわかりません。
SharedComponentDataの中身、本物を得たいのであれば更にT EntityManager.GetSharedComponentData(int sharedComponentIndex)にSharedComponentIndexを渡してあげましょう。
ただし、ECS version0.0.12-preview.16時点でSharedComponentData周りの実装はかなり未熟ですので、Dictionary<int, T>なりに毎フレームキャッシュしてあげた方がいいかもしれませんね。
#ComponentGroupに関する追記(2018/10/16)
実はComponentGroupを作成するComponentSystem.GetComponentGroupメソッドに新しいオーバーロードが追加されていました。
ComponentGroup GetComponentGroup(params EntityArchetypeQuery[] queries);
EntityArchetypeQueryの配列を受け取ってORでフィルタリングできます。
表記が簡単な上にフィルターとしての機能もChunk Iterationと同等になったので性能を追い求めないならばComponentGroupで十分でしょう。
また、NativeArray<ArchetypeChunk> ComponentGroup.CreateArchetypeChunkArray(Allocator allocator, out JobHandle handle)というAPIが追加されているので、無理にChunk Iterationせずとも良いのでは?と思っていたりします。
#具体例
Graphics.DrawMeshInstancedIndirectとComputeShaderを利用してそこそこ高速に描画する仕組みを実装してみました。
以下のシステムはPosition, Rotation, Scaleのうちいずれか1つを含み、かつMeshInstanceRendererIndexというSharedComponentDataを持つEntityを描画します。
MeshInstanceRendererInstancedIndirectSystemのコンストラクタにMeshInstanceRenderer[]を与えて初期化します。
つまり最初に何を描画するか決め打ちするのです。シーンで描画するものって案外事前にわかっているものですからね。
MeshInstanceRendererIndexは私が独自に定義したSharedComponentDataです。
これはMeshInstanceRendererInstancedIndirectSystem内部でのみ使用されることを想定していますのでクラス内で構造体を定義しています。
この構造体の取りうる値は1から描画する可能性のあるMeshInstanceRederer[].Lengthまでです。
SharedComponentDataManagerが付与するSharedComponentIndexと役割的に駄々被りですね。
ですが仕方ありません。SharedComponentIndexは割とフレーム毎に不定な上、厳密に連続した数値を取る保証がありません。 多少気を付ければフレーム間でSharedComponentIndexを固定できますが、管理の手間が面倒です。
故に厳密に連続した数値列でフレーム間で変化しないナンバリングを必要とする場合自分の手で車輪の再発明的に管理する必要があるのです。
public sealed class MeshInstanceRendererInstancedIndirectSystem : ComponentSystem
{
public readonly struct MeshInstanceRendererIndex : ISharedComponentData
{
public readonly uint Value;
public MeshInstanceRendererIndex(uint value) => Value = value == 0 ? throw new ArgumentOutOfRangeException() : value;
public MeshInstanceRendererIndex(int value) => Value = value <= 0 ? throw new ArgumentOutOfRangeException() : (uint)value;
}
public MeshInstanceRendererInstancedIndirectSystem(Camera activeCamera, MeshInstanceRenderer[] renderers, ComputeShader shader)
{
if ((this.activeCamera = activeCamera) == null) throw new ArgumentNullException("Camera must not be null!");
if ((this.renderers = renderers) == null) throw new ArgumentNullException("Renderers must not be null!");
if ((this.shader = shader) == null) throw new ArgumentNullException();
if (renderers.Length == 0) throw new ArgumentException("Length of the renderers must not be 0!");
buffers_Position = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
buffers_Position_Rotation = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
buffers_Position_Scale = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
buffers_Position_Scale_Rotation = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
buffers_Rotation = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
buffers_Scale = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
buffers_Scale_Rotation = new (ComputeBuffer, ComputeBuffer, ComputeBuffer, ComputeBuffer, int, int)[renderers.Length];
var tmpArgs = new uint[5];
for (int i = 0; i < buffers_Position.Length; i++)
{
ref var renderer = ref renderers[i];
tmpArgs[0] = renderer.mesh.GetIndexCount(renderer.subMesh);
(buffers_Position[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
(buffers_Scale[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
(buffers_Position_Scale[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
(buffers_Rotation[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
(buffers_Position_Rotation[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
(buffers_Scale_Rotation[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
(buffers_Position_Scale_Rotation[i].args = new ComputeBuffer(5, sizeof(uint), ComputeBufferType.IndirectArguments)).SetData(tmpArgs);
}
this.capacity = 1024;
}
private readonly Camera activeCamera;
private readonly ComputeShader shader;
private readonly EntityArchetypeQuery query = new EntityArchetypeQuery
{
None = Array.Empty<ComponentType>(),
Any = new[] { ComponentType.ReadOnly<Position>(), ComponentType.ReadOnly<Scale>(), ComponentType.ReadOnly<Rotation>() },
All = new[] { ComponentType.ReadOnly<MeshInstanceRendererIndex>() },
};
private readonly MeshInstanceRenderer[] renderers;
private readonly NativeList<EntityArchetype> foundArchetypes = new NativeList<EntityArchetype>(1024, Allocator.Persistent);
private static readonly int[] argsLength = new int[1];
private static readonly Bounds bounds = new Bounds(Vector3.zero, (float3)10000);
private static readonly MaterialPropertyBlock block = new MaterialPropertyBlock();
private static readonly int shaderProperty_PositionBuffer = Shader.PropertyToID("_PositionBuffer");
private static readonly int shaderProperty_ScaleBuffer = Shader.PropertyToID("_ScaleBuffer");
private static readonly int shaderProperty_RotationBuffer = Shader.PropertyToID("_RotationBuffer");
private static readonly int shaderProperty_TransformMatrixBuffer = Shader.PropertyToID("_TransformMatrixBuffer");
private static readonly int shaderProperty_ResultBuffer = Shader.PropertyToID("_ResultBuffer");
private readonly (ComputeBuffer args, ComputeBuffer positions, ComputeBuffer transforms, int count, int maxCount)[] buffers_Position;
private readonly (ComputeBuffer args, ComputeBuffer scales, ComputeBuffer transforms, int count, int maxCount)[] buffers_Scale;
private readonly (ComputeBuffer args, ComputeBuffer positions, ComputeBuffer scales, ComputeBuffer transforms, int count, int maxCount)[] buffers_Position_Scale;
private readonly (ComputeBuffer args, ComputeBuffer rotations, ComputeBuffer transforms, int count, int maxCount)[] buffers_Rotation;
private readonly (ComputeBuffer args, ComputeBuffer positions, ComputeBuffer rotations, ComputeBuffer transforms, int count, int maxCount)[] buffers_Position_Rotation;
private readonly (ComputeBuffer args, ComputeBuffer scales, ComputeBuffer rotations, ComputeBuffer transforms, int count, int maxCount)[] buffers_Scale_Rotation;
private readonly (ComputeBuffer args, ComputeBuffer positions, ComputeBuffer scales, ComputeBuffer rotations, ComputeBuffer transforms, int count, int maxCount)[] buffers_Position_Scale_Rotation;
private static readonly ProfilerMarker profileUpdateGatherChunks = new ProfilerMarker("Custom GatherChunks");
private static readonly ProfilerMarker profileUpdateDraw = new ProfilerMarker("Custom Draw");
private int capacity;
protected override void OnDestroyManager()
{
for (int i = 0; i < buffers_Position.Length; i++)
{
ref var bp = ref buffers_Position[i];
bp.args.Release();
if (bp.positions != null)
{
bp.positions.Release();
bp.transforms.Release();
}
ref var bs = ref buffers_Scale[i];
bs.args.Release();
if (bs.scales != null)
{
bs.scales.Release();
bs.transforms.Release();
}
ref var bps = ref buffers_Position_Scale[i];
bps.args.Release();
if (bps.positions != null)
{
bps.positions.Release();
bps.scales.Release();
bps.transforms.Release();
}
ref var br = ref buffers_Rotation[i];
br.args.Release();
if (br.rotations != null)
{
br.rotations.Release();
br.transforms.Release();
}
ref var bpr = ref buffers_Position_Rotation[i];
bpr.args.Release();
if (bpr.positions != null)
{
bpr.positions.Release();
bpr.rotations.Release();
bpr.transforms.Release();
}
ref var bsr = ref buffers_Scale_Rotation[i];
bsr.args.Release();
if (bsr.scales != null)
{
bsr.scales.Release();
bsr.rotations.Release();
bsr.transforms.Release();
}
ref var bpsr = ref buffers_Position_Scale_Rotation[i];
bpsr.args.Release();
if (bpsr.positions != null)
{
bpsr.positions.Release();
bpsr.scales.Release();
bpsr.rotations.Release();
bpsr.transforms.Release();
}
}
foundArchetypes.Dispose();
}
protected override void OnUpdate()
{
InitializeOnUpdate();
using (profileUpdateGatherChunks.Auto())
GatherChunks();
using (profileUpdateDraw.Auto())
Draw();
}
private void InitializeOnUpdate()
{
EntityManager.AddMatchingArchetypes(query, foundArchetypes);
for (int i = 0; i < buffers_Position.Length; i++)
{
buffers_Position[i].count = 0;
buffers_Position_Rotation[i].count = 0;
buffers_Position_Scale[i].count = 0;
buffers_Position_Scale_Rotation[i].count = 0;
buffers_Rotation[i].count = 0;
buffers_Scale[i].count = 0;
buffers_Scale_Rotation[i].count = 0;
}
}
private void ReAllocate<T>(ref ComputeBuffer buffer, int exSize) where T : struct
{
var _ = new T[exSize];
buffer.GetData(_, 0, 0, exSize);
buffer.Release();
buffer = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<T>());
buffer.SetData(_);
}
private void GatherChunks()
{
var PositionTypeRO = GetArchetypeChunkComponentType<Position>(true);
var ScaleTypeRO = GetArchetypeChunkComponentType<Scale>(true);
var RotationTypeRO = GetArchetypeChunkComponentType<Rotation>(true);
var MeshInstanceRendererIndexTypeRO = GetArchetypeChunkSharedComponentType<MeshInstanceRendererIndex>();
using (var chunks = EntityManager.CreateArchetypeChunkArray(foundArchetypes, Allocator.TempJob))
{
for (int i = 0; i < chunks.Length; i++)
{
var sharedIndex = chunks[i].GetSharedComponentIndex(MeshInstanceRendererIndexTypeRO);
// デフォルト値のSharedIndexは必ず0となる。
if (sharedIndex == 0) continue;
var rendererIndex = EntityManager.GetSharedComponentData<MeshInstanceRendererIndex>(sharedIndex).Value;
var pos = chunks[i].GetNativeArray(PositionTypeRO);
var scl = chunks[i].GetNativeArray(ScaleTypeRO);
var rot = chunks[i].GetNativeArray(RotationTypeRO);
int length = Math.Max(Math.Max(pos.Length, scl.Length), rot.Length);
if (length > capacity)
capacity = math.ceilpow2(length);
switch (-1 + (pos.Length > 0 ? 1 : 0) + (scl.Length > 0 ? 2 : 0) + (rot.Length > 0 ? 4 : 0))
{
case 0: // Position
Initialize_Position(rendererIndex, pos, length);
continue;
case 1: // Scale
Initialize_Scale(rendererIndex, scl, length);
continue;
case 2: // Position | Scale
Initialize_Position_Scale(rendererIndex, pos, scl, length);
continue;
case 3: // Rotation
Initialize_Rotation(rendererIndex, rot, length);
continue;
case 4: // Position | Rotation
Initialize_Position_Rotation(rendererIndex, pos, rot, length);
continue;
case 5: // Scale | Rotation
Initialize_Scale_Rotation(rendererIndex, scl, rot, length);
continue;
case 6: // Position | Scale | Rotation
Initialize_Position_Scale_Rotation(rendererIndex, pos, scl, rot, length);
continue;
default: continue;
}
}
}
}
private void Initialize_Position_Scale_Rotation(uint rendererIndex, NativeArray<Position> pos, NativeArray<Scale> scl, NativeArray<Rotation> rot, int length)
{
ref var buffer = ref buffers_Position_Scale_Rotation[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.positions = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.scales = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.rotations = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float3>(ref buffer.positions, buffer.count);
ReAllocate<float3>(ref buffer.scales, buffer.count);
ReAllocate<float4>(ref buffer.rotations, buffer.count);
buffer.maxCount = capacity;
}
buffer.positions.SetData(pos, 0, buffer.count, length);
buffer.scales.SetData(scl, 0, buffer.count, length);
buffer.rotations.SetData(rot, 0, buffer.count, length);
buffer.count += length;
}
private void Initialize_Scale_Rotation(uint rendererIndex, NativeArray<Scale> scl, NativeArray<Rotation> rot, int length)
{
ref var buffer = ref buffers_Scale_Rotation[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.scales = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.rotations = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float3>(ref buffer.scales, buffer.count);
ReAllocate<float4>(ref buffer.rotations, buffer.count);
buffer.maxCount = capacity;
}
buffer.scales.SetData(scl, 0, buffer.count, length);
buffer.rotations.SetData(rot, 0, buffer.count, length);
buffer.count += length;
}
private void Initialize_Position_Rotation(uint rendererIndex, NativeArray<Position> pos, NativeArray<Rotation> rot, int length)
{
ref var buffer = ref buffers_Position_Rotation[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.positions = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.rotations = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float3>(ref buffer.positions, buffer.count);
ReAllocate<float4>(ref buffer.rotations, buffer.count);
buffer.maxCount = capacity;
}
buffer.positions.SetData(pos, 0, buffer.count, length);
buffer.rotations.SetData(rot, 0, buffer.count, length);
buffer.count += length;
}
private void Initialize_Rotation(uint rendererIndex, NativeArray<Rotation> rot, int length)
{
ref var buffer = ref buffers_Rotation[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.rotations = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float4>(ref buffer.rotations, buffer.count);
buffer.maxCount = capacity;
}
buffer.rotations.SetData(rot, 0, buffer.count, length);
buffer.count += length;
}
private void Initialize_Position_Scale(uint rendererIndex, NativeArray<Position> pos, NativeArray<Scale> scl, int length)
{
ref var buffer = ref buffers_Position_Scale[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.positions = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.scales = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float3>(ref buffer.positions, buffer.count);
ReAllocate<float3>(ref buffer.scales, buffer.count);
buffer.maxCount = capacity;
}
buffer.positions.SetData(pos, 0, buffer.count, length);
buffer.scales.SetData(scl, 0, buffer.count, length);
buffer.count += length;
}
private void Initialize_Scale(uint rendererIndex, NativeArray<Scale> scl, int length)
{
ref var buffer = ref buffers_Scale[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.scales = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float3>(ref buffer.scales, buffer.count);
buffer.maxCount = capacity;
}
buffer.scales.SetData(scl, 0, buffer.count, length);
buffer.count += length;
}
private void Initialize_Position(uint rendererIndex, NativeArray<Position> pos, int length)
{
ref var buffer = ref buffers_Position[rendererIndex - 1];
if (buffer.transforms == null)
{
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
buffer.positions = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float3>());
buffer.maxCount = capacity;
}
else if (buffer.count + length > buffer.maxCount)
{
capacity = Math.Max(capacity, math.ceilpow2(buffer.count + length));
buffer.transforms.Release();
buffer.transforms = new ComputeBuffer(capacity, UnsafeUtility.SizeOf<float4x4>());
ReAllocate<float3>(ref buffer.positions, buffer.count);
buffer.maxCount = capacity;
}
buffer.positions.SetData(pos, 0, buffer.count, length);
buffer.count += length;
}
private void DrawInternal0(ComputeBuffer positions) => shader.SetBuffer(0, shaderProperty_PositionBuffer, positions);
private void DrawInternal1(ComputeBuffer scales) => shader.SetBuffer(1, shaderProperty_ScaleBuffer, scales);
private void DrawInternal2(ComputeBuffer positions, ComputeBuffer scales)
{
shader.SetBuffer(2, shaderProperty_PositionBuffer, positions);
shader.SetBuffer(2, shaderProperty_ScaleBuffer, scales);
}
private void DrawInternal3(ComputeBuffer rotations) => shader.SetBuffer(3, shaderProperty_RotationBuffer, rotations);
private void DrawInternal4(ComputeBuffer positions, ComputeBuffer rotations)
{
shader.SetBuffer(4, shaderProperty_PositionBuffer, positions);
shader.SetBuffer(4, shaderProperty_RotationBuffer, rotations);
}
private void DrawInternal5(ComputeBuffer scales, ComputeBuffer rotations)
{
shader.SetBuffer(5, shaderProperty_ScaleBuffer, scales);
shader.SetBuffer(5, shaderProperty_RotationBuffer, rotations);
}
private void DrawInternal6(ComputeBuffer positions, ComputeBuffer scales, ComputeBuffer rotations)
{
shader.SetBuffer(6, shaderProperty_PositionBuffer, positions);
shader.SetBuffer(6, shaderProperty_ScaleBuffer, scales);
shader.SetBuffer(6, shaderProperty_RotationBuffer, rotations);
}
private void DrawDispatch(int index, int count, ComputeBuffer args, ComputeBuffer transforms, int @case)
{
shader.SetBuffer(@case, shaderProperty_ResultBuffer, transforms);
int threadGroupsX = (int)(((uint)count) >> _SHIFT_);
if (threadGroupsX << _SHIFT_ == count)
shader.Dispatch(@case, threadGroupsX, 1, 1);
else shader.Dispatch(@case, threadGroupsX + 1, 1, 1);
ref var renderer = ref renderers[index];
Graphics.DrawMeshInstancedIndirect(renderer.mesh, renderer.subMesh, renderer.material, bounds, args, 0, block, renderer.castShadows, renderer.receiveShadows, 0, activeCamera);
}
private static void PreDraw(int count, ComputeBuffer args, ComputeBuffer transforms)
{
argsLength[0] = count;
args.SetData(argsLength, 0, 1, 1);
block.SetBuffer(shaderProperty_TransformMatrixBuffer, transforms);
}
private const int _SHIFT_ = 10;
private void Draw()
{
ComputeBuffer args, transforms;
int count;
for (int i = 0; i < buffers_Position.Length; i++)
{
if ((count = buffers_Position[i].count) == 0) goto SCALE;
PreDraw(count, args = buffers_Position[i].args, transforms = buffers_Position[i].transforms);
DrawInternal0(buffers_Position[i].positions);
DrawDispatch(i, count, args, transforms, 0);
SCALE:
if ((count = buffers_Scale[i].count) == 0) goto POSITION_SCALE;
PreDraw(count, args = buffers_Scale[i].args, transforms = buffers_Scale[i].transforms);
DrawInternal1(buffers_Scale[i].scales);
DrawDispatch(i, count, args, transforms, 1);
POSITION_SCALE:
if ((count = buffers_Position_Scale[i].count) == 0) goto ROTATION;
PreDraw(count, args = buffers_Position_Scale[i].args, transforms = buffers_Position_Scale[i].transforms);
DrawInternal2(buffers_Position_Scale[i].positions, buffers_Position_Scale[i].scales);
DrawDispatch(i, count, args, transforms, 2);
ROTATION:
if ((count = buffers_Rotation[i].count) == 0) goto POSITION_ROTATION;
PreDraw(count, args = buffers_Rotation[i].args, transforms = buffers_Rotation[i].transforms);
DrawInternal3(buffers_Rotation[i].rotations);
DrawDispatch(i, count, args, transforms, 3);
POSITION_ROTATION:
if ((count = buffers_Position_Rotation[i].count) == 0) goto SCALE_ROTATION;
PreDraw(count, args = buffers_Position_Rotation[i].args, transforms = buffers_Position_Rotation[i].transforms);
DrawInternal4(buffers_Position_Rotation[i].positions, buffers_Position_Rotation[i].rotations);
DrawDispatch(i, count, args, transforms, 4);
SCALE_ROTATION:
if ((count = buffers_Scale_Rotation[i].count) == 0) goto POSITION_SCALE_ROTATION;
PreDraw(count, args = buffers_Scale_Rotation[i].args, transforms = buffers_Scale_Rotation[i].transforms);
DrawInternal5(buffers_Scale_Rotation[i].scales, buffers_Scale_Rotation[i].rotations);
DrawDispatch(i, count, args, transforms, 5);
POSITION_SCALE_ROTATION:
if ((count = buffers_Position_Scale_Rotation[i].count) == 0) continue;
PreDraw(count, args = buffers_Position_Scale_Rotation[i].args, transforms = buffers_Position_Scale_Rotation[i].transforms);
DrawInternal6(buffers_Position_Scale_Rotation[i].positions, buffers_Position_Scale_Rotation[i].scales, buffers_Position_Scale_Rotation[i].rotations);
DrawDispatch(i, count, args, transforms, 6);
}
}
}
ComponentSystemのコンストラクタには描画する為のCameraコンポーネント、事前に決め打ちした描画内容のMeshInstanceRendererの配列、このComponentSystem専用のComputeShaderへの参照を与えています。
コンストラクタ内では引数、特にMeshInstanceRenderer[]を元にComputeBufferを何個も確保しています。
##Graphics.DrawMeshInstancedIndirectの使い方
Graphics.DrawMeshInstancedIndirectの公式リファレンス(英語)をまずは参照してください。
次に比較的わかりやすい具体例を参照してください。
私なりにGraphics.DrawMeshInstancedIndirectを利用した描画方法の流れを解説してみます。
- new ComputeBuffer(5, 4, ComputeBufferType.IndirectArguments)としてnewされたComputeBufferをargument bufferとこの記事では呼称します。
- argument bufferは公式レファレンスによるとsizeof(int)3=43=12byte以上の長さが必要です。実際の所、Graphics.DrawMeshInstancedIndirectで使用する場合は20byteの長さが最低でも必要でした。16byteで試してみた所何も言わずにUnityがクラッシュしました。
- argument bufferはuintまたはint型の変数5つが連続して並んでいる配列と見なせます。
- 各要素の意味は公式リファレンスを見てもわりと不明瞭ですが、Unityがシェーダーとか描画周りのAPIで元ネタにしているDirectXの資料でそれっぽいものがあります。
- 2~4は全て0で普通は大丈夫です。Meshの結合とかしていたら2,3を適切な値にするべきでしょう。
- 0:描画対象のMeshの頂点数
- 1:その描画対象を描く回数
- 2:uint Mesh.GetIndexStart(int subMesh)
- 3:uint Mesh.GetBaseVertex(int subMesh)
- 4:StartInstanceLocation 頂点バッファを読み始める前に個々のIndexに加算される値。筆者も意味がわかりません。
- 描画対象のMaterialが適切なComputeBufferを含んでいる。この記事の場合は「StructuredBuffer<float4x4> _ResultBuffer」。
- Material Property Blockあるいは直接MaterialにSetBufferメソッドでComputeBufferを設定します。
- 適切なNativeArray<T>をComputeBufferにSetDataします。
- argument bufferの[1]に描画回数をSetDataします。
- Graphics.DrawMeshInstancedIndirectの引数に適切な値を渡して実行します。
やることが案外多いのですね。
よく私が間違えてしまうポイントはargument bufferの[0]にMeshの頂点数を渡し忘れたり、全然別のMeshの頂点数を渡してしまいがちです。
頂点数を間違えると表示が崩れたり一切表示されなかったりしますので気を付けてください。
また、[1]にSetDataし忘れると表示回数が0のままとなり一切表示されませんのでこれもまた注意する必要があります。
##描画メソッド詳解
ArchetypeChunk.GetNativeArray<T>で戻ってくるNativeArray<T>のLengthプロパティを調べることでそのChunkがTというComponentDataを含んでいるかどうか調べられます。
3つのComponentDataを含むかどうかをそれぞれ調べてbit flag化すると1~7までの値を取り得ます。いずれも含まないということはありえませんので0は取り得ません。
このbit flagsから1を引きます。0~6までの値域となります。
この描画システムはComputeShaderを利用しています。ComputeShaderには複数のKernelを仕込むことが可能です。KernelはComputeShader内で宣言された順番に0インデックスでIDナンバーが付与されます。
bit flags-1とKernelのIDを対応させることで処理を明瞭にしました。
OnUpdateではChunkをIterationしつつComputeBufferにKernelに対応して情報を詰めていきます。
描画予定数がComputeBufferの要素数よりも多くなった場合ComputeBufferの再確保とデータコピーを行います。
この際にComputeBuffer.GetData()メソッドを使用するのですが、これはかなり遅いメソッドです。
多分ここをもうちょっと改善したらもっとこのシステムは早くなれます。
#Matrix4x4を計算するComputeShader
内部でやっていることは本当に単純な行列計算です。
#pragma kernel _Position
#pragma kernel _Scale
#pragma kernel _Position_Scale
#pragma kernel _Rotation
#pragma kernel _Position_Rotation
#pragma kernel _Scale_Rotation
#pragma kernel _Position_Scale_Rotation
#define _COUNT_ 1024
StructuredBuffer<float3> _PositionBuffer;
StructuredBuffer<float3> _ScaleBuffer;
StructuredBuffer<float4> _QuaternionBuffer;
RWStructuredBuffer<float4x4> _ResultBuffer;
[numthreads(_COUNT_,1,1)]
void _Position (uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float3 pos = _PositionBuffer[index];
_ResultBuffer[index] = float4x4(float4(1, 0, 0, pos.x), float4(0, 1, 0, pos.y), float4(0, 0, 1, pos.z), float4(0, 0, 0, 1));
}
[numthreads(_COUNT_,1,1)]
void _Scale(uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float3 scale = _ScaleBuffer[index];
_ResultBuffer[index] = float4x4(float4(scale.x, 0, 0, 0), float4(0, scale.y, 0, 0), float4(0, 0, scale.z, 0), float4(0, 0, 0, 1));
}
[numthreads(_COUNT_,1,1)]
void _Rotation(uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float4 n = normalize(_QuaternionBuffer[id.x]);
const float3 n2 = n.xyz * 2;
const float3 __ = n.xyz * n2;
const float3 w = n.w * n2;
const float xy = n.x * n2.y;
const float xz = n.x * n2.z;
const float yz = n.y * n2.z;
_ResultBuffer[id.x] = float4x4(
float4(1 - (__.y + __.z), xy - w.z, xz + w.y, 0),
float4(xy + w.z, 1 - (__.x + __.z), yz - w.x, 0),
float4(xz - w.y, yz + w.x, 1 - (__.x + __.y), 0),
float4(0, 0, 0, 1));
}
[numthreads(_COUNT_,1,1)]
void _Position_Rotation(uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float4 n = normalize(_QuaternionBuffer[index]);
const float3 pos = _PositionBuffer[index];
const float3 n2 = n.xyz * 2;
const float3 __ = n.xyz * n2;
const float3 w = n.w * n2;
const float xy = n.x * n2.y;
const float xz = n.x * n2.z;
const float yz = n.y * n2.z;
_ResultBuffer[index] = float4x4(
float4(1 - (__.y + __.z), xy - w.z, xz + w.y, pos.x),
float4(xy + w.z, 1 - (__.x + __.z), yz - w.x, pos.y),
float4(xz - w.y, yz + w.x, 1 - (__.x + __.y), pos.z),
float4(0, 0, 0, 1));
}
[numthreads(_COUNT_,1,1)]
void _Position_Scale(uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float3 pos = _PositionBuffer[index];
const float3 scale = _ScaleBuffer[index];
_ResultBuffer[index] = float4x4(
float4(scale.x, 0, 0, pos.x),
float4(0, scale.y, 0, pos.y),
float4(0, 0, scale.z, pos.z),
float4(0, 0, 0, 1)
);
}
[numthreads(_COUNT_,1,1)]
void _Scale_Rotation(uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float3 scale = _ScaleBuffer[index];
const float4 n = normalize(_QuaternionBuffer[index]);
const float3 n2 = n.xyz * 2;
const float3 __ = n.xyz * n2;
const float3 w = n.w * n2;
const float xy = n.x * n2.y;
const float xz = n.x * n2.z;
const float yz = n.y * n2.z;
const float3 r0 = float3(1 - (__.y + __.z), xy - w.z, xz + w.y);
const float3 r1 = float3(xy + w.z, 1 - (__.x + __.z), yz - w.x);
const float3 r2 = float3(xz - w.y, yz + w.x, 1 - (__.x + __.y));
_ResultBuffer[index] = float4x4(
float4(r0*scale, 0),
float4(r1*scale, 0),
float4(r2*scale, 0),
float4(0, 0, 0, 1)
);
}
[numthreads(_COUNT_,1,1)]
void _Position_Scale_Rotation(uint3 id : SV_DispatchThreadID)
{
const unsigned int index = id.x;
const float3 pos = _PositionBuffer[index];
const float3 scale = _ScaleBuffer[index];
const float4 n = normalize(_QuaternionBuffer[index]);
const float3 n2 = n.xyz * 2;
const float3 __ = n.xyz * n2;
const float3 w = n.w * n2;
const float xy = n.x * n2.y;
const float xz = n.x * n2.z;
const float yz = n.y * n2.z;
const float3 r0 = float3(1 - (__.y + __.z), xy - w.z, xz + w.y);
const float3 r1 = float3(xy + w.z, 1 - (__.x + __.z), yz - w.x);
const float3 r2 = float3(xz - w.y, yz + w.x, 1 - (__.x + __.y));
_ResultBuffer[index] = float4x4(
float4(r0*scale, pos.x),
float4(r1*scale, pos.y),
float4(r2*scale, pos.z),
float4(0, 0, 0, 1)
);
}
#この描画システムに対応させたStandardシェーダー
もともとのStandard Shaderからの変更点はStructuredBuffer _TransformMatrixBufferの存在とGPU Instancing対応です。
このStandardシェーダーは結構重いので影の描画をしないのであればUnlitシェーダーをベースに改造した方が明らかにFPSが改善されます。
Shader "Custom/Standard"
{
Properties
{
_Color("Color", Color) = (1,1,1,1)
_MainTex("Albedo", 2D) = "white" {}
_Cutoff("Alpha Cutoff", Range(0.0, 1.0)) = 0.5
_Glossiness("Smoothness", Range(0.0, 1.0)) = 0.5
_GlossMapScale("Smoothness Scale", Range(0.0, 1.0)) = 1.0
[Enum(Metallic Alpha,0,Albedo Alpha,1)] _SmoothnessTextureChannel ("Smoothness texture channel", Float) = 0
[Gamma] _Metallic("Metallic", Range(0.0, 1.0)) = 0.0
_MetallicGlossMap("Metallic", 2D) = "white" {}
[ToggleOff] _SpecularHighlights("Specular Highlights", Float) = 1.0
[ToggleOff] _GlossyReflections("Glossy Reflections", Float) = 1.0
_BumpScale("Scale", Float) = 1.0
_BumpMap("Normal Map", 2D) = "bump" {}
_Parallax ("Height Scale", Range (0.005, 0.08)) = 0.02
_ParallaxMap ("Height Map", 2D) = "black" {}
_OcclusionStrength("Strength", Range(0.0, 1.0)) = 1.0
_OcclusionMap("Occlusion", 2D) = "white" {}
_EmissionColor("Color", Color) = (0,0,0)
_EmissionMap("Emission", 2D) = "white" {}
_DetailMask("Detail Mask", 2D) = "white" {}
_DetailAlbedoMap("Detail Albedo x2", 2D) = "grey" {}
_DetailNormalMapScale("Scale", Float) = 1.0
_DetailNormalMap("Normal Map", 2D) = "bump" {}
[Enum(UV0,0,UV1,1)] _UVSec ("UV Set for secondary textures", Float) = 0
// Blending state
[HideInInspector] _Mode ("__mode", Float) = 0.0
[HideInInspector] _SrcBlend ("__src", Float) = 1.0
[HideInInspector] _DstBlend ("__dst", Float) = 0.0
[HideInInspector] _ZWrite ("__zw", Float) = 1.0
}
CGINCLUDE
#define UNITY_SETUP_BRDF_INPUT MetallicSetup
ENDCG
SubShader
{
Tags { "RenderType"="Opaque" "PerformanceChecks"="False" }
// ------------------------------------------------------------------
// Base forward pass (directional light, emission, lightmaps, ...)
Pass
{
Name "FORWARD"
Tags { "LightMode" = "ForwardBase" }
Blend [_SrcBlend] [_DstBlend]
ZWrite [_ZWrite]
CGPROGRAM
#pragma target 5.0
// -------------------------------------
#pragma shader_feature _NORMALMAP
#pragma shader_feature _ _ALPHATEST_ON _ALPHABLEND_ON _ALPHAPREMULTIPLY_ON
#pragma shader_feature _EMISSION
#pragma shader_feature _METALLICGLOSSMAP
#pragma shader_feature ___ _DETAIL_MULX2
#pragma shader_feature _ _SMOOTHNESS_TEXTURE_ALBEDO_CHANNEL_A
#pragma shader_feature _ _SPECULARHIGHLIGHTS_OFF
#pragma shader_feature _ _GLOSSYREFLECTIONS_OFF
#pragma shader_feature _PARALLAXMAP
#pragma multi_compile_fwdbase
#pragma multi_compile_fog
#pragma multi_compile_instancing
#pragma vertex vert
#pragma fragment fragBase
#include "UnityStandardCoreForward.cginc"
StructuredBuffer<float4x4> _TransformMatrixBuffer;
VertexOutputForwardBase vert(VertexInput v, uint id : SV_INSTANCEID)
{
v.vertex.xyz = mul(_TransformMatrixBuffer[id], float4(v.vertex.xyz, 1)).xyz;
return vertBase(v);
}
ENDCG
}
}
CustomEditor "StandardShaderGUI"
}
#次回予告
次回こそJobComponentSystemを解説します。