一つのマテリアルが同じオブジェクトで重複しているのをまとめる (Python for Blender クックブック)

# 一つのマテリアルが同じオブジェクトで重複しているのをまとめる

Pythonを使ってBlenderでできる事をまとめています。解説もあります。
[はじめに](https://qiita.com/KTakahiro1729/items/5de5f60982af37d23aa0)
[レシピ一覧](https://qiita.com/KTakahiro1729/items/57f4e1f5495c5e54d431)

Pythonの難易度: ★★★★☆
### 動機
「こことここは別のマテリアルにしようかなぁ」と思っていたけど、完成してみたらマテリアルが重複していることはありませんか？
編集モードでいちいち作業をするよりも、pythonにやらせましょう。

### 細かい仕様
- 対象のオブジェクトはアクティブと仮定できます。
- マテリアル一覧（マテリアルスロット）の中で重複しているものを除いてください。
- 各面が参照しているマテリアルは変えないでください。
- マテリアルのリンクは全てDataになっています。
- マテリアルが設定されていないもの（空のマテリアルスロット）も、まとめてください。

## サンプルコード

```python
import bpy
import bmesh

obj = bpy.context.active_object

# material_indexの新旧の参照テーブル(Look Up Table)を作成
new_mats = []
mat_lut = []
for mat_slot in obj.material_slots:
    mat = mat_slot.material
    if not mat in new_mats:
        new_mats.append(mat)
    mat_lut.append(new_mats.index(mat))

# materialを並べ替える
for i, mat in enumerate(new_mats):
    obj.material_slots[i].material = mat

# 面が参照するスロットを変更
bm = bmesh.new()
bm.from_mesh(obj.data)

for face in bm.faces:
    face.material_index = mat_lut[face.material_index]

bm.to_mesh(obj.data)

bm.free()

# スロットを選択して削除する
while len(obj.material_slots) > len(new_mats):
  obj.active_material_index = len(new_mats)
  bpy.ops.object.material_slot_remove()
```


上記サンプルコードを、Blender内蔵のテキストエディタに貼り付けた後、<kbd>Alt</kbd>-<kbd>P</kbd>を実行してください。

なお、手作業の内容をそのまま再現したものも作ることが出来ます。（そのうち書く）

## 解説
### マテリアルスロット
python APIにはオブジェクトが利用しているマテリアルを知る方法が2つあります。(それ以上あるかもしれないけど知らない)

1. `object.data.materials`
  `object.data`はメッシュを指すため、マテリアルがメッシュに紐付けられていることを示します。これはアウトラインで「メッシュの下にマテリアルがある様子」と同じです。
2. `object.material_slots[i].material`
  各オブジェクトは`material_slot`というリスト様の構造を所有しており、メッシュ内のマテリアルを順番に格納しています。これはマテリアルの設定画面の一番上にある表と同じです。

今回の「マテリアルが重複している」というのは、マテリアルスロット内の異なる場所に同じマテリアルが入っていることを指します。
ですので、2を主に使っていくことに成ります。

### 全体の流れ
このコードは

1. マテリアルスロットの新旧対応表を作る
2. 対応表に基づいて、各面に割り当てられたマテリアルスロットを変更する
  bmeshを使用
3. 不要なマテリアルスロットを削除する

の3部からなっています。

#### 新旧対応表 (look up table)
（look up tableはよく使われる用語ですが、新旧対応表は私の造語です）

簡単な例として、

```
[Mat.001, Mat.002, Mat.001, Mat.002, Mat.000]
```

という長さ5のマテリアルスロットを考えます。
重複を除くと

```
[Mat.001, Mat.002, Mat.000]
```

の3つだけになるので、さっきの5つはこの3つの内のどれか一つと同じものです。
例えば、先頭を0番目として数えると、元のスロットで`Mat.002`なのは、1番目と3番目です。
これは、新しいスロットでは1番目なので

```
[?, 1, ?, 1, ?]
```

を参照すれば良いことが分かります。他の2つに対しても同じことをすると

```
[0, 1, 0, 1, 2]
```

という対応表が得られます。


### `bmesh`
PythonからBlenderを使うには基本的に`bpy`を使いますが、それ以外にもいくつかのライブラリがあります。
例えば、`bmesh`はメッシュを扱うためのライブラリです。
`bpy`はオブジェクトモード用、`bmesh`は編集モード用のライブラリ、と考えてください。

bmeshを使うには

```python
bm = bmesh.new()
bm.from_mesh(obj.data)

# 処理

bm.to_mesh(obj.data)
bm.free()
```

という流れを踏みます。

#### `bmesh.new()`
`bmesh`で主に使う`BMesh`というオブジェクトを作成しています。
メッシュの実体というべきものです。
この段階では空っぽです。

#### `bm.from_mesh(obj.data)`
`BMesh`の`from_mesh(メッシュ)`というメソッドはメッシュ(data-block)から実際のメッシュを引き出して、保存します。
オブジェクトモード->編集モードの切り替えに似ています。
この段階でメッシュの様々なデータが格納されています。

#### `bm.to_mesh(obj.data)`
`from_mesh`とは逆に、実際のメッシュからメッシュ(data-block)をつくります。
編集モード->オブジェクトモードの切り替えに似ています。
`bm`の中身はもう不要ですが、（場合によっては大きいデータが）残っています。

#### `bm.free`
`bm`の中身を消します。
`del bm`と同じだと思われますが、公式サンプルではこうやってます。


### `bm.faces`・`face.material_index`
`BMesh`のプロパティー`faces`には`BMFace`という面を指すオブジェクトがリスト様に入っています。
`BMFace`にはその面積などを指す様々なプロパティーがありますが、今回はその面が参照しているマテリアルスロットの番号を指す`material_index`を書き換えました。

### `obj.active_material_index`
そのオブジェクトのアクティブなマテリアルスロットは意外なことに各オブジェクトに格納されています。

### `bpy.ops.object.material_slot_remove`
アクティブなオブジェクトのアクティブなマテリアルスロットを消す関数です。