Blenderでインポートしたマテリアルを一括調整

BlenderにFBX等でインポートしたオブジェクトには自動でマテリアルを設定されます。
ところが、やりとりするソフトによっては 値の解釈が違ったりと意図しない設定になる場合があるようです。

そこで、そういった設定を調整するためのスクリプトを作成してみました
これをそのまま使用するというよりは　環境に合わせて微調整してください

## Blenderのマテリアルについて。
Blenderをあまり利用しない人がこのスクリプトを使う機会が多いかと思うので
先にBlenderのマテリアルについて軽く触れておきます。

Blenderのオブジェクトには複数のマテリアルを持つことができて　面毎に割り当てを指定できます。
2.80以降のバージョンでは基本的にノードを使ってマテリアルを設定するようになっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/ae9e15b0-07e9-7964-5d14-2fc69ae5e68b.png)
インポートしたり新規にマテリアルを設定すると**プリンシプルBSDF**の設定がされた状態になっていますが
これは**PBR**（物理ベースレンダリング）を使うゲームエンジン等と互換性を持つように設計されたものになります。
エクスポートする場合にもこのプリンシプルBSDFとテクスチャのみでマテリアルを設定することになります。
## プリンシプルBSDF調整スクリプト
今回のものは　インポートした場合に設定されているプリンシプルBSDFの調整に絞って
バンプマップで設定されていたものがノーマルマップとして読み込まれるのを修正するのを中心に　値を調整する機能を作成してみました
シーン上で選択しているオブジェクトのマテリアルを全てを調整します。

```py:y_settingBSDF.py
import bpy
# BSDF_settingのコメントアウトした項目を設定値に書き換え
BSDF_settings = {
    # 'distribution':'GGX', #分布メソッドのプルダウン　[GGX','MULTI_GGX']
    # 'subsurface_method':'BURLEY', #サブサーフェイスメソッドのプルダウン　['BURLEY','RANDOM_WALK']
    'Base Color': [0.8, 0.8, 0.8, 1.0],
    # 'Subsurface':0.0,
    # 'Subsurface Radius':[1.0, 0.2, 0.1], 
    # 'Subsurface Color':[0.8, 0.8, 0.8, 1.0],
    # 'Metallic':0.0,
    # 'Specular':0.5,
    # 'Specular Tint':0.0,
    # 'Roughness':0.5,
    # 'Anisotropic':0.0,
    # 'Anisotropic Rotation':0.0,
    # 'Sheen':0.0,
    # 'Sheen Tint':0.5,
    # 'Clearcoat':0.0,
    # 'Clearcoat Roughness':0.03,
    # 'IOR':1.4500000476837158,
    # 'Transmission':0.0,
    # 'Transmission Roughness':0.0,
    # 'Emission':[0.0, 0.0, 0.0, 1.0],
    # 'Emission Strength':1.0,
    # 'Alpha':1.0,
    # 'Normal':[0.0, 0.0, 0.0],
    # 'Clearcoat Normal':[0.0, 0.0, 0.0],
    # 'Tangent':[0.0, 0.0, 0.0]
}

def adjust_material(material):
    '''設定に従ってマテリアルの調整'''
    node_tree = material.node_tree
    # 現在使用されているアウトプットの取得
    output_node = get_active_outputnode(node_tree)
    # サーフェイスアウトプットに直接繋がってるノードの取得
    socket_1 = get_from_socket(node_tree, output_node.inputs['Surface'])
    # 接続されていたものがプリンシプルBSDFだった場合設定を変更
    if socket_1.node.type == 'BSDF_PRINCIPLED':
        BSDF_node = socket_1.node
        ## 現在のノードの入力値を別の項目の設定と入れ替えたい場合get_node_inputs で値を辞書で取得
        # ref_node_inputs = get_node_inputs(BSDF_node)
        # BSDF_settings['Base Color'] = ref_node_inputs['Base Color']

        ## 不要なノード接続の削除 (例：スペキュラ接続されてるノードの接続を削除)
        remove_input_link(node_tree, BSDF_node.inputs['Specular'])

        # プリンシプルBSDFの値を設定の記述通りに変更
        set_BSDF_input(BSDF_node, BSDF_settings)
        # ノーマルへの入力を現在のものからテクスチャをバンプとして利用する方法に変更
        set_normal_socekt(node_tree, BSDF_node)

def get_material_list(objects):
    '''指定したオブジェクトについているマテリアルの一覧を取得'''
    material_list = []
    for obj in objects:
        if obj.type == 'MESH':
            for mat_slot in obj.material_slots:
                if not mat_slot.material in  material_list:
                    material_list.append(mat_slot.material)
    return material_list

def get_active_outputnode(node_tree):
    '''ノードツリー内のアクティブな出力を取得 '''
    for node_ in node_tree.nodes:
        if node_.type == 'OUTPUT_MATERIAL' and node_.is_active_output:
            return node_

def get_from_socket(node_tree, input_socket):
    '''入力に繋がっているリンクの反対側の出力ソケットの取得'''
    if input_socket.is_linked:
        for link_ in node_tree.links:
            if link_.to_socket == input_socket:
                return link_.from_socket

def remove_input_link(node_tree, input_socket):
    '''入力に繋がっているリンクを削除'''
    if input_socket.is_linked:
        for link_ in node_tree.links:
            if link_.to_socket == input_socket:
                node_tree.links.remove(link_)
    return

def set_BSDF_input(BSDF_node, BSDF_settings):
    '''BSDF_settings 記述した値にノードの設定を変更'''
    for input_name in BSDF_settings.keys():
        value = BSDF_settings[input_name]
        if input_name == 'distribution':
            BSDF_node.distribution = value
        elif input_name == 'subsurface_method':
            BSDF_node.subsurface_method = value
        else:
            BSDF_node.inputs[input_name].default_value = value
    return

def get_node_inputs(node):
    '''ノードのインプットに設定されている値を取得'''
    node_inputs = {}
    for node_input in node.inputs:
        if node_input.type in ['VALUE','STRING']:
            node_inputs[node_input.name] = node_input.default_value
        else:
            node_inputs[node_input.name] = list(node_input.default_value)
    return node_inputs

def set_normal_socekt(node_tree, BSDF_node):
    '''ノーマルマップで指定されているテクスチャをバンプ設定に'''
    # ノーマル入力に接続されているノードの判別
    normal_input = BSDF_node.inputs['Normal']
    socket_out = get_from_socket(node_tree, normal_input)
    if socket_out and socket_out.node.type == 'NORMAL_MAP':
        # バンプノードに組み換え
        normal_node = socket_out.node
        bump_node = node_tree.nodes.new('ShaderNodeBump')
        bump_node.location = [normal_node.location[0], normal_node.location[1] -200]
        # リンクの組み換え
        node_tree.links.new(bump_node.outputs[0], normal_input)
        texture_out = get_from_socket(node_tree, normal_node.inputs['Color'])
        if texture_out:
            node_tree.links.new(texture_out, bump_node.inputs['Height'])
    return

# マテリアルの一覧を取得
material_list = get_material_list(bpy.context.selected_objects)
# マテリアルのアウトプットにプリンシプルBSDFがついているかどうか
for material in material_list:
    adjust_material(material)
```

値指定で変更したい場合BSDF_settingsの辞書の中の設定を利用してください
get_node_inputs(BSDF_node)関数で現在設定されている値もソケット名の辞書で取得するようにしています。



## スクリプトから見たノード
ここからはスクリプトを改変したい人のためのメモになります

Blenderのスクリプトではマテリアルの中にあるノードの情報を格納するもの全体を**node_tree**と表現して
個々の箱型のパーツを**node** node同士を繋いでいる線を**link**　nodeにある入出力の部分を**socket**と呼んでいます
ノードは左側にあるソケットの入力値またはソケットの欄の横で設定されている値を元に内部で計算して右側に出力します

例えばスクリプトで
「現在の操作の　アクティブなオブジェクトの　アクティブなマテリアルの　プリンシプルBSDFというノード」を表すのは
```bpy.context.active_object.active_material.node_tree.nodes['Principled BSDF']```
といった感じで、プリンシプルBSDFの入力側のスペキュラの項目は
```bpy.context.active_object.active_material.node_tree.nodes['Principled BSDF'].inputs['Specular']```
といった具合で指定します
リンクは
```bpy.context.active_object.active_material.node_tree.links[0]```
のようにインデックスのみで指定できて from_socket　to_socket といった属性でつながっているノードを判別するようです。
今回はソケットを指定すると接続されてるノードを出力する関数を作成して接続状態の取得を扱いやすくしてみました

ノードを新規作成する場合は
node_tree.nodes.new([ノードの種類の名称])
ノードの接続は
node_tree.links.new([接続するソケット1],[接続するソケット1])
といった具合のようです

オブジェクトに設定されているマテリアルそれぞれを指定するのは
```[オブジェクト].material_slots[0].material```
なように　マテリアルスロットのインデックスを指定して取得します。

今回取り上げなかったノードを新規作成したい場合には
以前に既存のノードを記録するスクリプトも作成しましたのでそちらの出力を参照してもらえればと思います
https://qiita.com/yukimituki11/items/dc945cca0b1fc429aa07