【Unity DOTS】ECSでEntityのMaterialのプロパティーを変更したい

# ■概要
EntityのMaterialのプロパティーをコードから変更する。
(DOTS初学者なのでコードとか諸々:poop:かも)

# ■やること
1. HybridRendererの導入
2. ShaderGraphでMaterialを作成
3. PrefabからEntityを何個か作成して、それらのMaterialのプロパティーをSystemクラスからいじる
3. 猫の腹を撫でる

# ■サンップル
https://github.com/imaginechan2467/ECS-Material-Sample

![sample3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951067/11b51def-f536-45c0-932c-6875e8acbfc5.gif)


# ■環境
UnityEngine : 6000.0.23f1
Entities : 1.3.2
Entities Graphics : 1.3.2
Hybrid Renderer : 0.5.2-preview.4
猫 : 3歳


# ■リファレンス

↓ECSで変更したいMaterialの設定

https://docs.unity3d.com/Packages/com.unity.rendering.hybrid@0.50/manual/material-overrides-asset.html

↓コードでMaterial作る場合に必要そうなやつ。
ShaderGraphで作るならいらない。

https://docs.unity3d.com/ja/2022.3/Manual/dots-instancing-shaders.html


# ■導入

Entities、Entities Graphicsはパッケージマネージャーから入れられる。

:::note warn
Entitiesより先にEntities Graphicsを入れたほうが良いです。
Entities Graphicsを入れると、同時に同バージョンのEititiesも追加されます。
:::

しかし、現状HybridRendererはパッケージマネージャーから検索しても出てこない。
なので直接manifest.jsonに書き込む。
(プロジェクト名/Packages/manifest.json)
```
"com.unity.rendering.hybrid": "0.5.2-preview.4",
```

(この時点で赤文字がいっぱいでてきたりするけど怖いからClear。レッチュゴー)

# ■マテリアルの作成

今回はMaterialをShaderGraphで作成してます。

:::note warn
コードで作る場合は、ShaderGraphではデフォルトで設定されている部分も書く必要があるらしい。
:::

今回は適当にColor,float,Vector4<b>(*1)</b>のプロパティーを触れるマテリアルを作ってみる。

:::note warn
<b>(*1)</b>実際はvector2,vector3で良いプロパティーも現状エラーが出るのでvector4にしている。
:::


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951067/b7794be5-66d9-48a3-8e0c-a593b6c2a986.png)

#### EntityのMaterialプロパティーを変更するための設定
1.　GraphSettingsAllow material Overrideにチェック

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951067/4efd9257-1e6f-4c0d-b6d3-9e05eb410d30.png)

2.　プロパティーを選択した状態で、NodeSettingsのScopeをHybrid Per Instanceに設定する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951067/8ed50732-f485-4529-8883-810f3bd5382e.png)

#### MaterialOverrideAssetを作成する

1. ProjectWindowの中でCreate/Shader/MaterialOverrideAssetからMaterialOverrideAssetを作成する。
2. MaterialOverrideAssetに作ったMaterialをアタッチュする。 

:::note warn
ここでいきなり、AddPropertyOverrideからコードから変更したいプロパティーにチェックを入れてもいいのですが、それだとプロパティー毎に新規.csがアタッチしているMaterialと同じフォルダ内にできて気持ちが悪いので、今回は先にプロパティー用のコードを書いて自動で.csが作成されないようにする。
:::
:::note warn
でも1.csに複数Propertyをまとめたら、それはそれで同じ名前で同じタイプのプロパティーを書いてしまうことになりかねないので、MaterialOverrideProperties的なフォルダの中にプロパティー1個=1.csで管理する方が良い？かもわかめ
:::

<details><summary>コード</summary>

```
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;

namespace ECS_Material_Sample
{
    /// <summary>
    /// TestMaterialのSystem側から変更できるプロパティーをまとめている場所
    /// </summary>
    /// <remarks>
    /// - ↓これを自分で書かなくても、MaterialOverrideAssetでoverrideするプロパティー
    /// - にチェックを入れたら自動で同じコードが作られる。
    /// - けど以下の理由で↓みたいな感じで書いてみている。
    /// - 1. 一個のMaterialのプロパティーを一ファイルにまとめておいたほうが見やすい
    /// - 2. 自動作成だとプロパティー毎に出来た.csがMaterialのあるファイルにできるので気持ち悪い
    /// </remarks>
    
    
    [MaterialProperty("_MainColor")]
    public struct MaterialPropertyMainColor : IComponentData
    {
        public float4 color;
    }
    
    [MaterialProperty("_Scale")]
    public struct MaterialPropertyScale : IComponentData
    {
        public float scale;
    }
    
    [MaterialProperty("_BoxOffset")]
    public struct MaterialPropertyOffset : IComponentData
    {
        /// <summary>
        /// _OffsetはVector2でいいのだけれども...
        /// </summary>
        /// <remarks>
        /// - MaterialOverrideAssetでVector2,Vector3のプロパティーを
        /// - overrideしてみるとわかるのですが、なぜか全部Vector4が表示される。
        /// - そのまま実行するとsizeが違うよ的なエラーが出る。
        /// - [MaterialProperty]アトリビュートの第二引数にsizeがあるので
        /// - 8にするとか試したけどだめだったので、Materialのプロパティー自体はVector2
        /// - だけど、ここではfloat4にしている。Banana
        /// </remarks>
        public float4 offset;
    }
}
```

</details>

MaterialPropertyアトリビュートでプロパティーのReference名を指定する。

もしプロパティーの名前を間違って、存在しないものを書いてしまうとここで.csが作成されてしまう。
そうなった場合は作成された.csとMaterialOverrideAssetを消して、[MaterialProperty()]の名前を直してからMaterialOverrideAssetを作り直す。

Materialを設定しているプレファブにMaterialOverrideコンポーネントを追加して、作ったMaterialOverrideAssetをOverrideAssetにアタッチュ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951067/c2860df2-ae07-48d2-b3b7-5fd7a458af53.png)

これでMaterialの準備はOK:v::v:

# ■スクリプト

#### Authoring
サンプルだと簡易的にAuthoringクラスでCubeプレファブを10000個作る要求Entityを作っている。

<details><summary>コード</summary>

```
using Unity.Entities;
using UnityEngine;

namespace ECS_Material_Sample
{
    public struct SpawnRequestData : IComponentData
    {
        public uint spawnCount;
        public Entity prefabEntity;
    }
    
    public class SpawnPrefabAuthoring : MonoBehaviour
    {
        [SerializeField] private GameObject prefab = null;
        [SerializeField] private uint spawnCount = 100;
        
        private class Baker : Baker<SpawnPrefabAuthoring>
        {
            public override void Bake(SpawnPrefabAuthoring authoring)
            {
                if (authoring.prefab == null || authoring.spawnCount == 0)
                {
                    Debug.LogWarning("プレファブとスポーン数を設定してお金を入れてね");
                    return;
                }
                
                //スポーンリクエストEntity作成
                var requestEntity = GetEntity(TransformUsageFlags.None);

                //リクエスト内容設定
                AddComponent(requestEntity, new SpawnRequestData()
                {
                    prefabEntity = GetEntity(authoring.prefab, TransformUsageFlags.Renderable),
                    spawnCount = authoring.spawnCount
                });
            }
        }
    }
}
```

</details>

#### SpawnSystem
ここで指定数指定のプレファブのインスタンスを作っている。
同時に作ったインスタンスにMaterialのプロパティーを変更するためのコンポーネントをくっつけている。

<details><summary>コード</summary>

```
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Random = UnityEngine.Random;

namespace ECS_Material_Sample
{
    [BurstCompile]
    public partial class SpawnPrefabSystem : SystemBase
    {
        protected override void OnCreate()
        {
            RequireForUpdate<SpawnRequestData>();
        }

        [BurstCompile]
        protected override void OnUpdate()
        {
            var ecbs = World.GetOrCreateSystemManaged<BeginInitializationEntityCommandBufferSystem>();
            var ecbSpawn = ecbs.CreateCommandBuffer();
            var ecbDestroy = ecbs.CreateCommandBuffer();
            
            foreach (var (requestData, requestEntity) in
                     SystemAPI.Query<SpawnRequestData>().WithEntityAccess())
            {
                float spacing = 1.5f;
                int edgeCount = (int)math.ceil(math.pow(requestData.spawnCount, 1.0f/3.0f));
                
                for (int i = 0; i < requestData.spawnCount; i++)
                {
                    //プレファブのInstance作成
                    var instanceEntity = EntityManager.Instantiate(requestData.prefabEntity);

                    #region 位置を設定

                    // 3次元のインデックスを計算
                    int x = i % edgeCount;
                    int y = (i / edgeCount) % edgeCount;
                    int z = i / (edgeCount * edgeCount);

                    //transform設定
                    ecbSpawn.SetComponent(instanceEntity, new LocalTransform()
                    {
                        Position = new float3(
                            x * spacing,  // X座標
                            y * spacing,  // Y座標
                            z * spacing   // Z座標
                        ),
                        Rotation = quaternion.identity,
                        Scale = 1f 
                    });

                    #endregion

                    #region _MainColor用コンポーネント設定

                    //_MainColorプロパティーをいじるためのコンポーネントを追加
                    ecbSpawn.AddComponent(instanceEntity, new MaterialPropertyMainColor()
                    {
                        //Systemで変更しているので初期値は0
                        color = float4.zero
                    });

                    #endregion

                    #region _Offset用コンポーネント設定

                    //_Offsetを更新するためのランダムな方向を設定
                    var dir = Random.insideUnitCircle;
                    ecbSpawn.AddComponent(instanceEntity, new MaterialPropertyOffset()
                    {
                        offset = new float4(dir.x, dir.y, 0, 0)
                    });

                    #endregion

                    #region _Scale用コンポーネント設定

                    //_Scaleをここで初期化するのみ
                    ecbSpawn.AddComponent(instanceEntity, new MaterialPropertyScale()
                    {
                        scale = Random.Range(1,7)
                    });

                    #endregion
                }

                //リクエスト削除
                ecbDestroy.DestroyEntity(requestEntity);
            }
        }
    }
}
```
</details>

#### Materialの色プロパティーを更新するSystem

今までのやり方だとMaterial matでmat.SetColorとかSetFloatとかでやっていたのを、MaterialPropertyMainColorをSetCoponentしている箇所でやっている。

<details><summary>コード</summary>

```
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

namespace ECS_Material_Sample
{
    [BurstCompile]
    public partial class MaterialPropertyMainColorUpdateSystem : SystemBase
    {
        protected override void OnCreate()
        {
            RequireForUpdate<MaterialPropertyMainColor>();
        }

        [BurstCompile]
        protected override void OnUpdate()
        {
            var world = World.DefaultGameObjectInjectionWorld;
            var ecbs = world.GetOrCreateSystemManaged<EndSimulationEntityCommandBufferSystem>();
            var ecb = ecbs.CreateCommandBuffer();

            #region 色計算

            var time = (float)world.Time.ElapsedTime;
            const float speed = 0.1f;
            const float delay = 0.025f;
            
            float3 HsvToRgb(float h, float s, float v)
            {
                float3 rgb = math.abs(math.fmod(h * 6.0f + new float3(0.0f, 4.0f, 2.0f), 6.0f) - 3.0f) - 1.0f;
                rgb = math.saturate(rgb);
                return v * math.lerp(new float3(1.0f), rgb, s);
            }

            #endregion
            
            foreach (var (mainColorComponent, transform, entity) in 
                     SystemAPI.Query<RefRO<MaterialPropertyMainColor>,RefRO<LocalTransform>>().WithEntityAccess())
            {
                #region 色計算

                float hue = math.fmod(time * speed + transform.ValueRO.Position.y * delay, 1.0f);
                float saturation = 0.75f;
                float value = 0.75f;
                float3 rgb = HsvToRgb(hue, saturation, value);

                #endregion
                
                //ここでMaterialの色を変える要求を出している
                ecb.SetComponent(entity, new MaterialPropertyMainColor()
                {
                    color = new float4(rgb.x, rgb.y , rgb.z, 1f)
                });
            }
        }
    }
}
```
</details>


### ▲Entity関連のScriptが自作のAssemblyDefinitionに属している場合の参照設定について

大体追加する人たち
- Unity.Entities
- Unity.Burst
- Unity.Collections

今回みたいに[MaterialProperty("_Color")]とかするなら↓
- Unity.Entities.Graphics

物理挙動を使うならUnity Phisicsパッケージを入れて↓とか
- Unity.Physics

# ■詰みポ

<details><summary>Entities Graphics辺りのパッケージのバージョンでエラーが出る</summary>

:umbrella: \- Entities GraphicsをInstallしたはずなのに[Material Property("")]なんぞねえっていうエラーが出るし、Packages/Entities Graphicsの中身を見るとフォルダの中身が消えている。
:sunny: \- その場合は一旦Entities、Entities GraphicsをパッケージマネージャーからRemoveして、Entities Graphicsを再度Install。

</details>

<details><summary>loading entity scene failed...というエラーが出る</summary>

:umbrella: \- loading entity scene failed...というエラーが出て結果が見えない。

:sunny: \- 一度ProjectウィンドウからSubSceneをダブルクリックで開いた後、元のシーンを開き直してもっかい実行したら”行けたりする”。

</details>

# ■猫の腹を撫でる

![neko.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2951067/b513e333-73cc-4900-91c5-621511242c0c.gif)