Renderer.materialがマテリアルを複製する条件を調査する

# 環境
```
Unity2021.3.4f1
```

# `Renderer.material`のgetプロパティはマテリアルを複製する
ので`sharedMaterial`を使いましょう、というのはよく聞く話です。
正確な理解のために、その複製の挙動を調査してみましょう。

## 確認用シーンとコード
同じマテリアルをアタッチしたオブジェクトを並べます。（3つは要らんかった……）
![スクリーンショット 2022-09-07 14.20.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356775/fc833e12-544a-16e8-a4f3-5a7fcf01e30c.png)
適当に`material`や`sharedMaterial`にアクセスし、`GetInstanceID`でインスタンスの同一性を確認していきます。
```cs
using UnityEngine;

public class MaterialTest : MonoBehaviour
{
    [SerializeField] private Renderer _sphere0;
    [SerializeField] private Renderer _sphere1;
    [SerializeField] private Renderer _sphere2;

    private void Start()
    {
        // sharedMaterialのインスタンス確認（同じはず）
        Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
        Debug.Log($"(1, sharedMaterial) = {_sphere1.sharedMaterial.GetInstanceID()}");
        
        // materialのGetterにアクセスしてみる（複製されて別インスタンスになってるはず）
        Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");
        
        // もう一度sharedMaterialのインスタンス確認（0の方が変わってるはず）
        Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
        Debug.Log($"(1, sharedMaterial) = {_sphere1.sharedMaterial.GetInstanceID()}");
        
        // もう1度materialのGetterにアクセスしてみる（複製される？されない？）
        Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");
        Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
    }
}
```
### 結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356775/3d3323b4-f6d5-7329-6b08-a8d440849eb8.png" width="500">

1回目の`material`のgetプロパティアクセスで確かにマテリアルは複製されました。その後`sharedMaterial`にアクセスしても`material`にアクセスしても同じく複製後のマテリアルインスタンスが取得できました。2回目の`material`のgetプロパティアクセスでは複製は起きていないようですね。

# 複製される条件を探る
この結果から「2回目は複製されない」と言ってしまうのは少し安直な気がします。他の理由で複製されなかったのかもしれませんし、実は1回目じゃなくても複製されることがあるかもしれません。

## 1. 既に複製されたマテリアルが割り当てられている場合
最初から複製されたマテリアルをアタッチしていたらどうなるでしょう。
例えば以下の感じで事前にマテリアルを複製しておきます。（これはUnityに怒られますが、ここの手段はどうでもいいです）
```cs
[ContextMenu("Test/マテリアルチェンジ！")]
public void Test()
{
    _ = _sphere0.material;
}
```
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356775/213e38b3-8c38-5109-744b-bd949b19f75c.png" width="500">

`(Instance)`が付いてるので確かにマテリアルが複製されています。（まだエディタは再生していません。）
ではここから先程の確認コードを実行します。

### 結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356775/c4464647-c36a-a810-e4a6-5abb2a12ada7.png" width="500">

1回目の`material`のgetプロパティで複製され、2回目では複製されていないようです。getプロパティで複製が行われるかの判定には、今割り当てられているマテリアルが複製されたものかどうか（アセットかどうか）は関係無さそうです。

## 2. マテリアルをsetして、その後getする場合
setプロパティを使ってマテリアルを割り当てた後のgetプロパティはどのように動くでしょうか。
なお、`material`と`sharedMaterial`のsetプロパティは全く同じメソッド呼び出しを行なっているので、どちらを使っても変わりません。以下は`Renderer`クラスの抜粋です。
```cs
public Material material
{
    get
    {
        if (IsPersistent())
        {
            Debug.LogError("Not allowed to access Renderer.material on prefab object. Use Renderer.sharedMaterial instead", this);
            return null;
        }
        return GetMaterial();
    }
    set { SetMaterial(value); }
}

public Material sharedMaterial { get { return GetSharedMaterial(); } set { SetMaterial(value); } }
```
名前が違うのに同じ挙動というのはとても気持ち悪いですが、事実どちらを使っても同じなのでとりあえずsetプロパティは`material`の方を使って検証することにします。

では1回setしてから`material`のgetプロパティにアクセスしてみます。
```cs
private void Start()
{
    // 同じMaterialインスタンスを指してる確認
    Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
    Debug.Log($"(1, sharedMaterial) = {_sphere1.sharedMaterial.GetInstanceID()}");

    // materialのsetプロパティにアクセスしてみる
    var newMat = new Material(_sphere0.sharedMaterial);
    _sphere0.material = newMat;
    Debug.Log(newMat.GetInstanceID());
        
    // materialのgetプロパティにアクセスしてみる
    Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");

    // sharedMaterialを確認
    Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
    Debug.Log($"(1, sharedMaterial) = {_sphere1.sharedMaterial.GetInstanceID()}");
        
    // 改めてmaterialとsharedMaterialの値を確認
    Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");
    Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
}
```
### 結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356775/9992b031-38b1-0989-d722-e8acafbcd6ae.png" width="550">

setプロパティで割り当てたインスタンス`newMat`と、その直後にgetプロパティで取得したインスタンスは別物、つまりここで複製が起きているようです。

## 3. 1回getして、その後setして、さらにその後getした場合
もうやってることがめちゃくちゃですが、とりあえず確認しておきます。
```cs
private void Start()
{
    // 同じMaterialインスタンスを指してる確認
    Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
    Debug.Log($"(1, sharedMaterial) = {_sphere1.sharedMaterial.GetInstanceID()}");
        
    // materialのGetterにアクセスしてみる（ここで複製されるはず）
    Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");

    // materialのSetterにアクセスしてみる
    var newMat = new Material(_sphere0.sharedMaterial);
    _sphere0.material = newMat;
    Debug.Log(newMat.GetInstanceID());
        
    // materialのGetterにアクセスしてみる（ここがどうなるか）
    Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");

    // 違うMaterialインスタンスを指してる
    Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
    Debug.Log($"(1, sharedMaterial) = {_sphere1.sharedMaterial.GetInstanceID()}");
        
    // 連続したmaterialのGetterアクセスは直前と同じものが返ってくるはず
    Debug.Log($"(0, material) = {_sphere0.material.GetInstanceID()}");
    Debug.Log($"(0, sharedMaterial) = {_sphere0.sharedMaterial.GetInstanceID()}");
}
```
### 結果
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2356775/bff56abc-a350-52ea-4876-b7c291a424f6.png" width="600">

最初の`material`のgetで複製が起こるのは当然ですが、さらに1度setを挟むと次のgetでも複製が起きているようです。

# `Renderer.material`の設計思想（個人の解釈です）
`Renderer.material`プロパティは「この`Renderer`インスタンスだけ見た目を変えたい！」という要求に応えるものとして設計されています。最初からアタッチされていたマテリアルや外部からセットされたマテリアルは他の`Renderer`にもアタッチされているかもしれませんから、複製して「自分だけのマテリアルインスタンス」を作ることで対応する、という感じです。でももちろん
```cs
_sphere2.material = _sphere0.material;
```
みたいにすれば複製されたマテリアルを他`Renderer`インスタンスに渡すことは可能です。

# `Renderer.material`はgetもsetも使わない
検証でなんとなく動きや設計思想が見えた感じはしますが、もしかしたらまだ理解が不十分かもしれません（UnityがC++部分を公開してくれればいいんですが……）。それに、インスタンスの状態によって動きが変わるのにそれが外からわからないAPIは単純に怖いです。複製されたマテリアルはこっちで破棄しなきゃいけないですから無視もできません。

ということで、**materialプロパティは使わない**でいいと思います。マテリアルインスタンスの差し替えを行いたければ素直に`new Material`してsetしましょう。
`material`と`sharedMaterial`のsetは中身が同じですが、いちいち「これはsetだから`material`を使っても大丈夫」とか考えたくないので、setも`sharedMaterial`を使いましょう。

# 補足: 配列版プロパティについて
`Renderer`には`materials`と`sharedMaterials`というマテリアル配列を返すプロパティもありますが、こちらも、複製されうるマテリアルインスタンスの数が1つから複数になるだけで同様の議論が成り立ちます。`materials`はやはり使わないで良いでしょう。

ちなみに`materials`と`sharedMaterials`のgetプロパティは配列を返すUnityAPIなので、例に漏れずアクセスのたびにGCAllocします。そして例に漏れず事前に確保した配列（今回はListですが）を引数に取るNonAlloc版のAPIも用意されているので、頻繁に呼び出す必要がある場合はNonAlloc版である`GetSharedMaterials`がお得です。

https://docs.unity3d.com/ja/2021.2/ScriptReference/Renderer.GetSharedMaterials.html