2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-09-07

環境

Unity2021.3.4f1

Renderer.materialのgetプロパティはマテリアルを複製する

のでsharedMaterialを使いましょう、というのはよく聞く話です。
正確な理解のために、その複製の挙動を調査してみましょう。

確認用シーンとコード

同じマテリアルをアタッチしたオブジェクトを並べます。(3つは要らんかった……)
スクリーンショット 2022-09-07 14.20.14.png
適当にmaterialsharedMaterialにアクセスし、GetInstanceIDでインスタンスの同一性を確認していきます。

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()}");
    }
}

結果

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

複製される条件を探る

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

1. 既に複製されたマテリアルが割り当てられている場合

最初から複製されたマテリアルをアタッチしていたらどうなるでしょう。
例えば以下の感じで事前にマテリアルを複製しておきます。(これはUnityに怒られますが、ここの手段はどうでもいいです)

[ContextMenu("Test/マテリアルチェンジ!")]
public void Test()
{
    _ = _sphere0.material;
}

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

結果

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

2. マテリアルをsetして、その後getする場合

setプロパティを使ってマテリアルを割り当てた後のgetプロパティはどのように動くでしょうか。
なお、materialsharedMaterialのsetプロパティは全く同じメソッド呼び出しを行なっているので、どちらを使っても変わりません。以下はRendererクラスの抜粋です。

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プロパティにアクセスしてみます。

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()}");
}

結果

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

3. 1回getして、その後setして、さらにその後getした場合

もうやってることがめちゃくちゃですが、とりあえず確認しておきます。

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()}");
}

結果

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

Renderer.materialの設計思想(個人の解釈です)

Renderer.materialプロパティは「このRendererインスタンスだけ見た目を変えたい!」という要求に応えるものとして設計されています。最初からアタッチされていたマテリアルや外部からセットされたマテリアルは他のRendererにもアタッチされているかもしれませんから、複製して「自分だけのマテリアルインスタンス」を作ることで対応する、という感じです。でももちろん

_sphere2.material = _sphere0.material;

みたいにすれば複製されたマテリアルを他Rendererインスタンスに渡すことは可能です。

Renderer.materialはgetもsetも使わない

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

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

補足: 配列版プロパティについて

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

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

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?