【Unity】SetPropertyBlockメソッドの挙動詳細

# はじめに

ソースコードからRenderer系コンポーネントに設定されたMaterialのプロパティを設定する場合、主に以下の三つの方法があります。

1. `sharedMaterials`プロパティや`sharedMaterial`プロパティで取得したMaterialにプロパティを設定する
1. `materials`プロパティや`material`プロパティで取得したMaterialのコピーにプロパティを設定する
1. `Renderer`クラスが持つ`SetPropertyBlock`メソッドを使用して設定する

一つ目の方法は、Materialのコピーは発生しないためメモリ使用量は一番少なくなります。**しかし、同じMaterialを参照している他のRendererにも変更が反映されてしまうため、使用できる場面は限られます。**この理由から、`sharedMaterials`系プロパティで取得したMaterialに対するプロパティの変更は公式ドキュメントで非推奨とされています[^2]。

二つ目の方法は、同じMaterialを参照している他のRendererが存在する場合にMaterialが丸ごとクローンされるため、他のRendererに変更が反映されてしまうことはありません[^3]。Materialがクローンされる分メモリの使用量は多くなります。

三つ目の方法は、Material自体のプロパティを変更することなくプロパティを上書きする方法です。Material全体をクローンする必要がないので、メモリ使用量の面で二つ目の方法よりも効率的になります[^1]。少数のパラメーターを変更するだけの場合は、この方法を使用することが公式ドキュメントで推奨されています[^1]。

本記事では三つ目の方法について扱います。

`SetPropertyBlock`メソッドには、Renderer単位で設定するper-rendererな設定方法とMaterial単位で設定するper-materialな設定方法の二種類が存在します[^1]。これらの設定方法を併用した場合にどうなるのか、PropertyBlockが設定されているかどうかを取得する`HasPropertyBlock`メソッドはこれらの設定方法によって挙動が変わるのか、そのあたりが少しわかりづらく感じました。
本記事ではそのあたりについて調査してまとめています。

# 検証環境

- Unity 2020.3.12f1

# まとめ

急いでいる人向けにまとめを最初に書いておきます。

per-renderとper-materialの両方で設定した場合は、per-materialが優先されます。優先の判断はMaterial単位で行われます。
`HasPropertyBlock`メソッドはper-renderかper-materialかに関わらず、PropertyBlockが設定されていれば`true`になります。

|per-renderの設定|per-materialの設定|反映される方|HasPropertyBlock()|
|:---|:---|:---|:---|
|してない|してない|なし|false|
|した|してない|per-render|true|
|してない|した|per-material|true|
|した|した|per-material|true|

# 挙動詳細

以下の四つの場合について調べました。

- なにも設定しない場合
- per-renderで設定した場合
- per-materialで設定した場合
- per-renderとper-materialで同時に設定した場合

視覚的にわかりやすいよう、左半分にインデックス0のMaterial、右半分にインデックス1のMaterialを割り当てた球を用意し、それぞれの場合にプロパティの変更がどのように反映されているか例として示しています。インデックス0のMaterialも1のMaterialも標準色は白です。

## なにも設定しない場合

`HasPropertyBlock()`はもちろん`false`になります。

なにも設定していないので、左右どちらも標準の白色になっています。
![default.jpg.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76085/fe9cbf6b-7b43-19e1-5e93-67df6366aea6.png)

## per-renderで設定した場合

引数にMaterialインデックスを渡さない場合は、Renderer全体のスコープでプロパティが設定されるため、すべてのMaterialに反映されます。
`HasPropertyBlock()`は`true`になります。

以下のようなコードでper-renderで赤色を設定すると、全てのMaterialに反映されるため、インデックス0と1両方のMaterialに色が反映され、全体が赤くなります。

```cs:per-render.cs
var mb = new MaterialPropertyBlock();
mb.SetColor("_Color", Color.red);
renderer.SetPropertyBlock(mb);
```

![per-render.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76085/1403cc2b-e618-2c9c-8a20-05186db45e17.png)

## per-materialで設定した場合

引数にMaterialインデックスを渡した場合は、Materialのスコープでプロパティが設定されるため、指定したインデックスのMaterialにのみ反映されます。
`HasPropertyBlock()`は`true`になります。

以下のようなコードでインデックス0のMaterialのみに緑色を設定すると、インデックス0のMaterialにのみ反映されるため、左側が緑になります。

```cs:per-material.cs
var mb = new MaterialPropertyBlock();
mb.SetColor("_Color", Color.green, 0);
renderer.SetPropertyBlock(mb);
```

![per-material.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76085/9368cf78-8c5e-febf-a8a6-1fe644fa66bf.png)

## per-renderとper-materialで同時に設定した場合

per-materialが優先して使われます。
この時に、優先度の判断はプロパティごとではなくMaterialごとに行われます。
つまり、per-renderとper-materialの両方を設定している状態で、per-renderで定義されていてper-materialで定義されていないプロパティがある場合、そのプロパティはper-materialのPropertyBlockが参照されて定義されていないものとして扱われます。

`HasPropertyBlock()`は`true`になります。

以下のようなコードでper-renderで赤を、インデックス0のMaterialに緑色を設定すると、インデックス0のMaterialにはper-renderとper-materialの両方が設定されますが、per-materialが優先されるため左側が緑になります。インデックス1のMaterialにはper-renderのみが設定されているので、右側が赤になります。

```cs:per-material.cs
{
	var mb = new MaterialPropertyBlock();
	mb.SetColor("_Color", Color.red);
	mb.SetFloat("_Metallic", 1);
	renderer.SetPropertyBlock(mb);
}
{
	var mb = new MaterialPropertyBlock();
	mb.SetColor("_Color", Color.green);
	renderer.SetPropertyBlock(mb, 0);
}
```

![per-render-and-material.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/76085/309133c5-fb47-b3d4-383e-60d879da4b96.png)

おしまい

[^1]: [Unity APIリファレンス「Renderer.SetPropertyBlock」](https://docs.unity3d.com/ja/2020.3/ScriptReference/Renderer.SetPropertyBlock.html)
[^2]: [Unity APIリファレンス「Renderer.sharedMaterials」](https://docs.unity3d.com/ja/2020.3/ScriptReference/Renderer-sharedMaterials.html)
[^3]: [Unity APIリファレンス「Renderer.materials」](https://docs.unity3d.com/ja/2020.3/ScriptReference/Renderer-materials.html)