【Blender】Python スクリプトでシェーダーノードの接続を変更する #1

投稿者: | 2023-05-17

ChatGPTを使って、シェーダーノードの接続を切り替えるスクリプトを作りました。

テクスチャをベイクするときに、Bake Typeを「Diffuse」にすると、メタリックの影響で色が暗くなります。

Renderプロパティ
Metallic : 0
Metallic : 0
Metallic : 1
Metallic : 1

そこで、Base Colorと接続しているソケットをMaterial OutputのSurfaceと直接繋げて、Bake Eypeを「Emit」にしてベイクすることがあります。

通常
Surfaceに接続
Emitでベイク

また、メタリックは専用のBake Typeが無いので、やはりSurfaceに直接つなげてBake Typeを「Emit」にしてベイクします。

このとき、メタリックへの入力がなくデフォルトの値が使われている場合、Valueノードを新しく追加して、それをSurfaceに接続して「Emit」でベイクしています。

ノーマルマップは、通常通りの接続でBake Typeを「Normal」にしてベイクします。

ベイク元のハイポリのオブジェクトが複数あり、それらに複数のマテリアルがついていたりすると、切り替えるのに時間がかかります。そこで、ChatGPTを活用して、スクリプトで自動化してみました。

スクリプト

import bpy

from enum import Enum

class TextureMapType(Enum):
    Albedo = 'Base Color'
    Metallic = 'Metallic'
    Roughness = 'Roughness'
    Normal = 'Normal'

map_type = TextureMapType.Albedo;

def get_or_create_value_node(mat, name):
    # すでに同じ名前のノードがあればそれを返す
    for node in mat.node_tree.nodes:
        if node.name == name:
            if isinstance(node, bpy.types.ShaderNodeValue):
                return node
    
    # 同じ名前のノードがなければ新しいノードを作成する
    value_node = mat.node_tree.nodes.new(type="ShaderNodeValue")
    value_node.name = name
    value_node.label = name
    value_node.location = (-300, 0)  # 位置を調整する
    return value_node

def connect_material_nodes(mat):

    # Principled BSDF ノードを取得
    principled_bsdf = mat.node_tree.nodes.get('Principled BSDF')

    if map_type == TextureMapType.Normal:
        output = principled_bsdf.outputs[0]
        
    else:
        if principled_bsdf.inputs[map_type.value].is_linked:
            output = principled_bsdf.inputs[map_type.value].links[0].from_socket
        else:
            value_node = get_or_create_value_node(mat, 'DefaultValueNode')
            value_node.location = (principled_bsdf.location[0]-200, principled_bsdf.location[1]+200) 

            value_node.outputs[0].default_value = principled_bsdf.inputs[map_type.value].default_value 
            output = value_node.outputs[0]

    # Material Output ノードを取得
    material_output = mat.node_tree.nodes.get('Material Output')

    # node1 のアウトプットを Material Output の Surface に接続
    mat.node_tree.links.new(output, material_output.inputs['Surface'])


# 選択されたすべてのオブジェクトを取得
selected_objects = bpy.context.selected_objects

# オブジェクトのマテリアルをすべて取得
all_materials = []
for obj in selected_objects:
    for slot in obj.material_slots:
        if slot.material not in all_materials:
            all_materials.append(slot.material)

# 取得したすべてのマテリアルの名前を表示
for material in all_materials:
    connect_material_nodes(material)

まず、ベイクするテクスチャの種類を表す列挙体を作りました。それによって処理を切り替えます。

import bpy

from enum import Enum

class TextureMapType(Enum):
    Albedo = 'Base Color'
    Metallic = 'Metallic'
    Roughness = 'Roughness'
    Normal = 'Normal'

map_type = TextureMapType.Albedo;

今回は、map_type変数の値を書き換えてテクスチャのタイプを変更します。

メタリックやラフネスの接続がないときは、Valueノードを新規作成します。Valueノードには固有の名前を付けて、同じ名前のValueノードがあれば再利用しています。ノードに固有の名前が付ける方法があるか質問しています。

def get_or_create_value_node(mat, name):
    # すでに同じ名前のノードがあればそれを返す
    for node in mat.node_tree.nodes:
        if node.name == name:
            if isinstance(node, bpy.types.ShaderNodeValue):
                return node
    
    # 同じ名前のノードがなければ新しいノードを作成する
    value_node = mat.node_tree.nodes.new(type="ShaderNodeValue")
    value_node.name = name
    value_node.label = name
    value_node.location = (-300, 0)  # 位置を調整する
    return value_node

メソッド名やコメントなども含めてChatGPTが書いたものを、少しだけ修正しています。

マテリアルのnode_tree.nodesですべてのノードのコレクションが得られるので、名前が同じValueノードがあればそれを返し、なければ新しく作っています。

次に、ベイクするテクスチャのタイプに合わせて、ノードをつなぎなおすメソッドを定義しました。

def connect_material_nodes(mat):

    # Principled BSDF ノードを取得
    principled_bsdf = mat.node_tree.nodes.get('Principled BSDF')

    if map_type == TextureMapType.Normal:
        output = principled_bsdf.outputs[0]
        
    else:
        if principled_bsdf.inputs[map_type.value].is_linked:
            output = principled_bsdf.inputs[map_type.value].links[0].from_socket
        else:
            value_node = get_or_create_value_node(mat, 'DefaultValueNode')
            value_node.location = (principled_bsdf.location[0]-200, principled_bsdf.location[1]+200) 

            value_node.outputs[0].default_value = principled_bsdf.inputs[map_type.value].default_value 
            output = value_node.outputs[0]

    # Material Output ノードを取得
    material_output = mat.node_tree.nodes.get('Material Output')

    # node1 のアウトプットを Material Output の Surface に接続
    mat.node_tree.links.new(output, material_output.inputs['Surface'])

このメソッドでは、ノードツリーからPrincipled BSDFノードを取得した後、ベイクするテクスチャのタイプによって出力ソケットを変えています。

ノーマルマップの場合は、Principled BSDFノードの出力を使います。

    # Principled BSDF ノードを取得
    principled_bsdf = mat.node_tree.nodes.get('Principled BSDF')

    if map_type == TextureMapType.Normal:
        output = principled_bsdf.outputs[0]

それ以外の場合は、テクスチャのタイプに対応した「Base Color」や「Metallic」などの入力ソケットに接続があれば、その出力ソケットにします。

    else:
        if principled_bsdf.inputs[map_type.value].is_linked:
            output = principled_bsdf.inputs[map_type.value].links[0].from_socket

Node.inputsはソケットのコレクションで、インデックスとしてソケット名を使うとソケットを取得でき、そのリンクのfrom_socketで、接続している出力ソケットが得られるようです。

接続がなければ、上のメソッドでValueノードをつくり、位置を調整してから、そのソケットの値をValueノードに入れて、Valueノードのアウトプットを使います。

        else:
            value_node = get_or_create_value_node(mat, 'DefaultValueNode')
            value_node.location = (principled_bsdf.location[0]-200, principled_bsdf.location[1]+200) 

            value_node.outputs[0].default_value = principled_bsdf.inputs[map_type.value].default_value 
            output = value_node.outputs[0]

その後、マテリアルアウトプットノードを取得して、上で得たアウトプットソケットと、マテリアルアウトプットノードのSurfaceという入力ソケットを接続しています。接続には、NodeLinks.newメソッドの引数に2つの両端のソケットを渡します。

    # Material Output ノードを取得
    material_output = mat.node_tree.nodes.get('Material Output')

    # node1 のアウトプットを Material Output の Surface に接続
    mat.node_tree.links.new(output, material_output.inputs['Surface'])

このメソッドでは、条件分岐の部分はまず自分で作りましたが、はじめにできたコードを送るとChatGPTがoutput変数の初期化方法を変更してくれました。

# 修正前
output = principled_bsdf.inputs[map_type].links[0].from_socket

if output == null:
    value_node = tree.nodes.new(type="ShaderNodeValue")
    value_node.location = (0, 0)  # ノードの位置を指定する        
    if map_type == TextureMapType.Mettalic:
        value_node.outputs[0].default_value = principled_bsdf.inputs['Metallic'].default_value             
    elif map_type == TextureMapType.Roughness:
        value_node.outputs[0].default_value = principled_bsdf.inputs['Roughness'].default_value 
    output = value_node.outputs[0]
#----

# ChatGPTが修正後
if principled_bsdf.inputs[map_type].is_linked:
    output = principled_bsdf.inputs[map_type].links[0].from_socket
else:
    value_node = mat.node_tree.nodes.new(type="ShaderNodeValue")
    value_node.location = (0, 0)  # ノードの位置を指定する        
    if map_type == TextureMapType.Metallic:
        value_node.outputs[0].default_value = principled_bsdf.inputs['Metallic'].default_value             
    elif map_type == TextureMapType.Roughness:
        value_node.outputs[0].default_value = principled_bsdf.inputs['Roughness'].default_value 
    output = value_node.outputs[0]

そして、アウトライナーなどで複数選択したすべてのオブジェクトのすべてのマテリアルを取得し、接続を変えるメソッドの引数に渡します。

# 選択されたすべてのオブジェクトを取得
selected_objects = bpy.context.selected_objects

# オブジェクトのマテリアルをすべて取得
all_materials = []
for obj in selected_objects:
    for slot in obj.material_slots:
        if slot.material not in all_materials:
            all_materials.append(slot.material)

# 取得したすべてのマテリアルの名前を表示
for material in all_materials:
    connect_material_nodes(material)

接続を切り替える

3つのオブジェクトを作って、複数のマテリアルを割り当てました。

Principled BSDFノードのアウトプットと、マテリアルアウトプットノードのSurfaceが接続されています。

アウトライナーでこれらのオブジェクトを複数選択して、テキストエディタの再生アイコンをクリックすると、接続が変更されました。

Principled BSDFノードのアウトプットのリンクを切れており、Principled BSDFノードのBase Colorソケットと接続している出力ソケットからもう一本のリンクが延びて、マテリアルアウトプットノードのSurfaceと接続されています。

マップのタイプをメタリックに書き換えて、もう一度オブジェクトを複数選択し、再生アイコンをクリックしました。

すると、Metallicへの接続がない場合は、新たに追加されたValueノードがマテリアルアウトプットに接続されました。

接続がある場合は、Metallicに接続されている出力ソケットがSurfaceに接続されています。Base Colorからの2本目のリンクはなくなっています。

ラフネスやノーマルでもうまくいきました。

Roughness
Normal

これでスクリプトでシェーダーノードの接続を切り替えられました。

今回はまず小さなまとまりごとにChatGPTにコーディングしてもらい、それを自分で組み立てるようにして作りましたが、新しいノードを作るなどの基本的なコードを事前に学習しなくても素早くスクリプトが作れました。

コメントを残す

メールアドレスが公開されることはありません。