マテリアルをアルファベット順に並べる (Python for Blender クックブック)

# マテリアルをアルファベット順に並べる
Pythonを使ってBlenderでできる事をまとめています。解説もあります。
[はじめに](https://qiita.com/KTakahiro1729/items/5de5f60982af37d23aa0)
[レシピ一覧](https://qiita.com/KTakahiro1729/items/57f4e1f5495c5e54d431)

Pythonの難易度: ★★★★☆

### 動機
様々にマテリアルを試していると、整理したくなりませんか？
なのに、Blenderのマテリアル（正確にはマテリアルスロット）の並べ替えって上下移動キーしかなくて不便じゃないですか？
（頂点グループとかはあるのに）

Pythonを使ってパッとやっちゃいましょうか！

### 細かい仕様
- 対象のオブジェクトはアクティブと仮定できます。
- 対象のオブジェクトには複数のマテリアルがセットされています。
- マテリアル名の比較はpythonの文字列比較に準じます。
- 重複しているマテリアル名は元の順番を維持してください。
- 空いているマテリアルスロットは最後にまとめてください。

## サンプルコード

```python
import bpy
obj = bpy.context.object
arr = obj.material_slots

def comp_swap(arr, i):
    """マテリアルスロットのマテリアルの名前を比較して、pythonの文字列比較上、若い方を上にする関数
    
    引数
    arr: iterable = 並べ替えるリスト（直接操作する）
    i  : int      = arrのiとi+1番目を比較／交換する

    返り値なし
    """
    # マテリアルスロットが空だとNoneを返すのでその処理
    are_none = (arr[i].material is None, arr[i+1].material is None)
    if are_none in [(True, True), (False, True)]:
        swap = False
    elif are_none in [(True, False)]:
        swap = True
    else:
        # マテリアル名に応じて入れ替えるかを決める
        if arr[i+1].material.name < arr[i+1].material.name:
            swap = True
        else:
            swap = False
    if swap:
        obj.active_material_index = i
        bpy.ops.object.material_slot_move(direction='DOWN')


for i in range(len(arr)):
    for j in range(len(arr)-2, i-1, -1): # range(i,len(arr)-1)をひっくり返したもの
        comp_swap(arr, j)
```


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

## 解説
### `bpy.ops.object.material_slot_move`
調べた限りでは、マテリアルスロットの順番を入れ替えられる唯一の方法です。

```python
obj.material_slots = sorted(mat_slots, key = lambda ms: ms.material.name if ms.material is not None else "")
```

とか行けるのかな、と思ったら、`material_slots`はread-onlyだと怒られました。
結局、自分でソートするしかない、ということになります。

### obj

### バブルソート
今回並べ替えに使ったのは、バブルソートと呼ばれるアルゴリズムです。

ソースコード中の`comp_swap`を次のように書き換えると、これは数字を並べ替えるプログラムに変身します。

```python
def comp_swap(arr, i):
    """大小比較して、小さい方を前にする関数
    
    引数
    arr: iterable = 並べ替えるリスト（直接操作する）
    i  : int      = arrのiとi+1番目を比較／交換する

    返り値なし
    """
    if arr[i+1] < arr[i]:
        arr[i], arr[i+1] = arr[i+1], arr[i]

# ソート用の関数を作ってシャッフル
import random
arr = list(range(10))
random.shuffle(arr)
print("before:", arr)
for i in range(len(arr)):
    print(arr[:i],arr[i:])
    for j in range(len(arr)-2, i-1, -1): # range(i,len(arr)-1)をひっくり返したもの
        comp_swap(arr,j)
print("after :", arr)
```

一旦、内側のfor文の中身が何なのかを無視して、出力だけ読むと

```python
before: [5, 9, 4, 3, 1, 7, 6, 0, 8, 2]
[] [5, 9, 4, 3, 1, 7, 6, 0, 8, 2]
[0] [5, 9, 4, 3, 1, 7, 6, 2, 8]
[0, 1] [5, 9, 4, 3, 2, 7, 6, 8]
[0, 1, 2] [5, 9, 4, 3, 6, 7, 8]
[0, 1, 2, 3] [5, 9, 4, 6, 7, 8]
[0, 1, 2, 3, 4] [5, 9, 6, 7, 8]
[0, 1, 2, 3, 4, 5] [6, 9, 7, 8]
[0, 1, 2, 3, 4, 5, 6] [7, 9, 8]
[0, 1, 2, 3, 4, 5, 6, 7] [8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8] [9]
after : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

のようなのが出ていると思います。（randomを使っているので、出力が多少違っても気にしないでください）

最初の`before: `というのは、ソート前のぐちゃぐちゃのリスト、`after:`というのはソート後のキレイなリストです。
その間のは、forループの中で出力しています。ここに注目してみましょう。

リストの前半部分はfor文ごとに一つ大きくなります。
また、この前半部分はどうやら`0`から順番に数字が入っています。

ここから内側のfor文が、どうやら「後ろのリストから一番小さいのを持ってきて、前のリストに追加する」関数、のようです。
しかし、よくよく見ると、後半のリストも若干変わっています。

では内側のfor文の中身は何でしょうか？

内側のfor文は、「後ろから小さいだけを移動してくる」という処理をします。
イメージとしては、

1. 現在連れている一番小さい数と一緒に、後ろから前へと移動する
2. 移動中により小さい数を見つけたら、それまで連れていた数と交換して、より小さい数を連れていく

このようにして、バブルソートでは「後々の処理のことも考えつつ、一番小さいのを持ってくる」という処理をします。

上の2.を実際に行っているのが`comp_swap`という関数です。ここを書き換えることで、交換する基準や交換する方法が選べます。
実際のサンプルコードでは、交換する基準は「マテリアルの名前」、交換する方法が「マテリアルの上下移動」としています。

なお、ソートのアルゴリズムはこの他にもたくさんあります。
理解がしやすいものを挙げると選択ソートや挿入ソート、速いものを挙げるとクイックソートやマージソート、アホみたいなのがボゴソート。
その中でバブルソートを選んだのは

- わかりやすい（既存のソートの中では2番目くらいにわかりやすい）
- 入れ替えの動作が隣接する要素同士（`material_slot_move`を使う上で便利）
- 十分速い（クイックソートとかに比べたらだいぶ遅いけど）
- 同名の要素でも順番を保ってくれる（安定である、といいます）

という理由です。
これを機会に他のソートも調べてみてください。