Blenderのシェーダノードをテキストで記録

Blendernのシェーダーを作成して頒布等したい場合
現状では.blendファイルで頒布して　読み込みたい環境でアペンドする必要があります。

.blendファイルの頒布は手間ですし、標準のスクリプトからの作成法では煩雑で
Qiitaのようなブログ記事にしたり、ノードを活用したスクリプトを書くのは手間がかかりました
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/7e2be873-8297-c8e2-325f-b29005260d9c.png)
なので、ノードで作成したデータをテキストとして書き出して
書き出したデータから再度ノードを生成するスクリプトを作成しました。
# ノード書き出しスクリプト
```PY:y_Nodetree2tex.py
import bpy
# 取得する属性の一覧
attr_list = ['bl_idname', 'name', 'location', 'attribute_name','image', 'object' , 'operation',  'uv_map']
"""
主なノード属性
'bl_idname'      :Blenderでノードを作成するための名前
'name'           :ノードの名前
'label'          :ノードのラベル
'height','width' :ノードの大きさ
'location'       :ノードの位置
'attribute_name' :属性ノードの入力値
'image'          :画像テクスチャで指定している画像
'object'         :テクスチャ座標等で指定しているオブジェクト
'operation'      :計算の種類等の設定
'uv_map'         :UVマップ
'parent'         :親のフレーム等
'select'         :選択しているか
"""

# 画像テクスチャノードの画像も記録したい場合は 'image'　を追加
def get_nodes(node_tree, attr_list):
    nodes = list(node_tree.nodes)
    node_settings = "[\n"
    for n in nodes:
        dic_ = {}
        for attr in attr_list:
            if  hasattr(n, attr):
                val_ = getattr(n,attr)
                dic_[attr] = val_
        if  hasattr(n, "inputs") and n.bl_idname != 'ShaderNodeOutputMaterial':
            list_ = []
            for i_ in n.inputs:
                if hasattr(i_, 'default_value'):
                    if i_.type in ['VALUE','STRING']:
                        list_ .append( i_.default_value ) 
                    else:
                        list_ .append( list(i_.default_value) )
            dic_["inputs"] = list_
        node_settings += str(dic_) + ',\n'
    node_links = []
    for L in node_tree.links:
        if L.from_socket.is_output:
            from_node_id = nodes.index(L.from_node)
            to_node_id = nodes.index(L.to_node)
            from_soc = list(L.from_socket.node.outputs).index(L.from_socket)
            to_soc = list(L.to_socket.node.inputs).index(L.to_socket)
        else:
            to_node_id = nodes.index(L.from_node)
            from_node_id = nodes.index(L.to_node)
            to_soc = list(L.from_socket.node.inputs).index(L.from_socket)
            from_soc = list(L.to_socket.node.outputs).index(L.to_socket)      
        node_links.append( [from_node_id, from_soc, to_node_id, to_soc] )
    node_settings += str({"node_linkes" : node_links}) + ',\n'
    node_settings +=  ']'
    return( node_settings )

# ノードの情報を取得
# アクティブオブジェクトのノード
node_tree = bpy.context.active_object.active_material.node_tree
# ワールドのノードを取得する場合
#node_tree =bpy.contextscene.world.node_tree
# 特定のグループノード内を取得する場合
#node_tree = bpy.data.node_groups[0]

txt = get_nodes(node_tree, attr_list)
print(txt)
# テキストをクリップボードに
#bpy.context.window_manager.clipboard = txt
```
アクティブオブジェクトのアクティブなノード　つまり　シェーダーエディタ画面で表示しているノードの状態を記録するスクリプトです
「テキストをクリップボードに」の部分をコメントアウトすると　実行時に結果をクリップボードに送ります
例えば　こんなノードで実行すると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/151493/35e13ce4-99e5-4d52-842c-12d98d265325.png)

```Py
[
{'bl_idname': 'ShaderNodeNewGeometry', 'name': 'Geometry', 'location': Vector((-990.0, 100.0)), 'inputs': []},
{'bl_idname': 'ShaderNodeVectorMath', 'name': 'Vector Math', 'location': Vector((-800.0, 130.0)), 'operation': 'SUBTRACT', 'inputs': [[0.0, 0.0, 0.0], [0.0, -1.0, 1.0], [0.0, 0.0, 0.0], 1.0]},
{'bl_idname': 'ShaderNodeSeparateXYZ', 'name': 'Separate XYZ', 'location': Vector((-600.0, 100.0)), 'inputs': [[0.0, 0.0, 0.0]]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math', 'location': Vector((-200.0, 210.0)), 'operation': 'MULTIPLY_ADD', 'inputs': [0.5, 0.5, 0.5]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.001', 'location': Vector((-200.0, 0.0)), 'operation': 'MULTIPLY_ADD', 'inputs': [0.5, 0.5, 0.5]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.002', 'location': Vector((-1120.0, 300.0)), 'operation': 'DIVIDE', 'inputs': [90.0, 2.0, 0.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.003', 'location': Vector((-940.0, 300.0)), 'operation': 'RADIANS', 'inputs': [0.5, 0.5, 0.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.004', 'location': Vector((-760.0, 300.0)), 'operation': 'TANGENT', 'inputs': [0.5, 0.5, 0.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.005', 'location': Vector((-580.0, 300.0)), 'operation': 'MULTIPLY_ADD', 'inputs': [0.5, 1.0, -2.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.006', 'location': Vector((-400.0, 300.0)), 'operation': 'DIVIDE', 'inputs': [0.5, 0.5, 0.0]},
{'bl_idname': 'ShaderNodeTexImage', 'name': 'Image Texture', 'location': Vector((200.0, 180.0)), 'image': bpy.data.images['Untitled'], 'inputs': [[0.0, 0.0, 0.0]]},
{'bl_idname': 'ShaderNodeEmission', 'name': 'Emission', 'location': Vector((480.0, 180.0)), 'inputs': [[1.0, 1.0, 1.0, 1.0], 1.0]},
{'bl_idname': 'ShaderNodeOutputMaterial', 'name': 'Material Output', 'location': Vector((660.0, 180.0))},
{'bl_idname': 'ShaderNodeCombineXYZ', 'name': 'Combine XYZ', 'location': Vector((0.0, 130.0)), 'inputs': [0.0, 0.0, 0.0]},
{'node_linkes': [[0, 0, 1, 0], [1, 0, 2, 0], [2, 0, 3, 0], [2, 1, 4, 0], [5, 0, 6, 0], [6, 0, 7, 0], [2, 2, 8, 0], [7, 0, 8, 1], [8, 0, 9, 1], [9, 0, 3, 1], [9, 0, 4, 1], [3, 0, 13, 0], [4, 0, 13, 1], [11, 0, 12, 0], [10, 0, 11, 0], [13, 0, 10, 0]]},
]
```
といったテキストにします。

注意が必要なのは　 ```'image': bpy.data.images['Untitled']``` のように
画像テクスチャやテクスチャ座標のオブジェクトを記録していますが
実行先で目的のオブジェクトがない場合にエラーになります

# ノード生成スクリプト

```Py:y_tex2NodeTree.py
import bpy
from mathutils import Vector
# データからノード情報の作成
def make_nodes(node_tree, node_set):
    node_list = []
    for n_ in node_set:
        if "bl_idname" in n_:
            # ノードの作成
            node = node_tree.nodes.new( n_.pop("bl_idname") )
            # アトリビュートの設定
            for attr_name in n_.keys():
                if  hasattr(node, attr_name):
                    if attr_name == "inputs":
                        for i_,var_ in enumerate(n_[attr_name]):
                            if var_: node.inputs[i_].default_value = var_
                    elif attr_name in ['image', 'object' , 'operation',  'uv_map']:
                        try:setattr(node, attr_name, n_[attr_name])
                        except:print('%s:%s was not set' % (node.name, attr_name))
                    else: setattr(node, attr_name, n_[attr_name])
            node_list.append(node)
        if "node_linkes" in n_:
            for L in n_["node_linkes"]:
                node_tree.links.new( node_list[ L[0] ].outputs[ L[1] ], node_list[ L[2] ].inputs[ L[3] ] )


obj = bpy.context.active_object
node_tree = obj.active_material.node_tree

#新規にノードグループを作成して　その中にノードを生成する場合
#node_tree = bpy.data.node_groups.new('ProjectionNode', 'ShaderNodeTree')

angle = 90
pos_ = [0,-1.0,2.0]

node_set = [
{'bl_idname': 'ShaderNodeNewGeometry', 'name': 'Geometry', 'location': Vector((-990.0, 100.0)), 'inputs': []},
{'bl_idname': 'ShaderNodeVectorMath', 'name': 'Vector Math', 'location': Vector((-800.0, 130.0)), 'operation': 'SUBTRACT', 'inputs': [[0.0, 0.0, 0.0], pos_ , [0.0, 0.0, 0.0], 1.0]},
{'bl_idname': 'ShaderNodeSeparateXYZ', 'name': 'Separate XYZ', 'location': Vector((-600.0, 100.0)), 'inputs': [[0.0, 0.0, 0.0]]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math', 'location': Vector((-200.0, 210.0)), 'operation': 'MULTIPLY_ADD', 'inputs': [0.5, 0.5, 0.5]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.001', 'location': Vector((-200.0, 0.0)), 'operation': 'MULTIPLY_ADD', 'inputs': [0.5, 0.5, 0.5]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.002', 'location': Vector((-1120.0, 300.0)), 'operation': 'DIVIDE', 'inputs': [angle, 2.0, 0.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.003', 'location': Vector((-940.0, 300.0)), 'operation': 'RADIANS', 'inputs': [0.5, 0.5, 0.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.004', 'location': Vector((-760.0, 300.0)), 'operation': 'TANGENT', 'inputs': [0.5, 0.5, 0.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.005', 'location': Vector((-580.0, 300.0)), 'operation': 'MULTIPLY_ADD', 'inputs': [0.5, 1.0, -2.0]},
{'bl_idname': 'ShaderNodeMath', 'name': 'Math.006', 'location': Vector((-400.0, 300.0)), 'operation': 'DIVIDE', 'inputs': [0.5, 0.5, 0.0]},
{'bl_idname': 'ShaderNodeTexImage', 'name': 'Image Texture', 'location': Vector((200.0, 180.0)), 'image': '', 'inputs': [[0.0, 0.0, 0.0]]},
{'bl_idname': 'ShaderNodeEmission', 'name': 'Emission', 'location': Vector((480.0, 180.0)), 'inputs': [[1.0, 1.0, 1.0, 1.0], 1.0]},
{'bl_idname': 'ShaderNodeOutputMaterial', 'name': 'Material Output', 'location': Vector((660.0, 180.0))},
{'bl_idname': 'ShaderNodeCombineXYZ', 'name': 'Combine XYZ', 'location': Vector((0.0, 130.0)), 'inputs': [0.0, 0.0, 0.0]},
{'node_linkes': [[0, 0, 1, 0], [1, 0, 2, 0], [2, 0, 3, 0], [2, 1, 4, 0], [5, 0, 6, 0], [6, 0, 7, 0], [2, 2, 8, 0], [7, 0, 8, 1], [8, 0, 9, 1], [9, 0, 3, 1], [9, 0, 4, 1], [3, 0, 13, 0], [4, 0, 13, 1], [11, 0, 12, 0], [10, 0, 11, 0], [13, 0, 10, 0]]},
]

# クリップボードのテキストを利用して生成したい場合
#node_set = bpy.context.window_manager.clipboard

#ノードの作成
make_nodes(node_tree, node_set)
```
ノード生成用のスクリプトの例です。
node_set = 以降に書き出しスクリプトの方で出力したテキストをペーストして一部調整してあります。

書き出したテキストがPythonのリストと辞書形式に準じているので
スクリプト内にペーストすることで利用できます。
当然書き出した値を変数で置き換えることもできます。