見出し画像

BlenderでノードツリーをXMLで出力して再度インポートするPythonをChatGPTが書いたお話

こんにちは、S.Fukaです。
早速ですがこの記事は「ChatGPTにBlenderのノードツリーをXMLで出力して、好きなノードツリーにインポートできるようにしてもらったお話」です。

今思えば、どうしてXMLにしたんだろう?正直、JSONでよかったんじゃないか…と思うけど、それは後でChatGPTに伝えることにします(笑)

ちなみにコードを書いてもらった結論としては最後の『ChatGPTにコードを書いてもらうことでわかった感動と課題、テクニック』に書いてあるので、結論だけ読みたい人はそこからご覧ください。

今回のコードを書いてもらったきっかけ

実は最初、XMLの入出力までやってもらおうとは思っていませんでした。
そもそも今回この機能を作ってもらった目的は…

「ノードツリーをPythonで取得してノードを再配置できたら便利機能作れそう!」

という比較的軽い気持ちです。

しかし、その気軽さから時間を割くのが面倒で放置していたところ、ChatGPTならうまくやってくれるのではと思い、試しに依頼してみることにしました。

最初のステップ:ノード一覧の出力

ちなみに使用したChatGPTはGPT-4です。
ひとまず簡潔に、自分の要望を伝えてみます。
それがこちらです。

最初の要望

先に言ってしまうと、一発で成功とはなりませんでした。
出力されたコードを実行してみると普通にエラー…この段階でさすがに無理か?とちょっとだけ思いました。

けれど試しに返ってきたエラーコードをそのまChatGPTに伝えたところ一発で見事修正してくれます。
それがこちらです。

import bpy

# 現在選択中のオブジェクトを取得
selected_obj = bpy.context.active_object

# 選択されたオブジェクトの選択中のマテリアルを取得
material = selected_obj.active_material

if material:
    # ノードツリーを取得
    node_tree = material.node_tree

    # ノードツリー内のノードを一覧表示
    print("ノードの一覧:")
    for node in node_tree.nodes:
        print("  -", node.name)

    # ノードツリー内のリンクを一覧表示
    print("\nリンクの一覧:")
    for link in node_tree.links:
        print("  - {0} ({1}) --> {2} ({3})".format(link.from_node.name, link.from_socket.name, link.to_node.name, link.to_socket.name))
else:
    print("選択されたオブジェクトにマテリアルが割り当てられていません。")

これを実行したところ次のようなものが出力されました。

ノードの一覧:
  - Principled BSDF
  - Material Output

リンクの一覧:
  - Principled BSDF (BSDF) --> Material Output (Surface)

どうでしょう。こんなの簡単だと思われるかもしれませんが、数分でこの結果が返ってくるのは驚異的です。

次のステップ:ノードの生成

次にやってみたのは出力されたテキストからのノード生成です。
私は次のようなことをChatGPTに伝えました。

ChatGPTへの次の指示

今見るとちょっと誤字ってますが、ChatGPTは問題なく答えてくれました。
それどころか今回のコードに関しては、エラーもなくノードが配置されリンクも繋がっていました。
さらなる衝撃です。

とはいえこの段階ではノードの座標すら出力していないのですべて重なって配置されています。

その後の進捗:概要

その後、更にコードに修正を加えていきました。
ただ、そのすべての流れを書くと記事が非常に長くなってしまうため、ここではやり取りの概要だけ紹介します。

  1. 出力された配置用コードだと新規マテリアルを作成してしまったので既存のノードツリーへ配置するよう依頼→修正

  2. すでに配置してあるノードと同名のノードを設置しないようになっていたので修正を依頼→エラー→修正

  3. より複数種類のノードが配置されたノードツリーを出力(問題なし)

  4. ↑の出力を元に配置→エラー→修正

  5. ノードの座標も出力するよう依頼→修正

  6. 出力された座標を元に配置するよう依頼→エラー→修正

  7. XML形式での出力を依頼→エラー→修正

  8. XMLを元に配置できるよう依頼→エラー→修正

  9. ノードに設定されていた値も出力するよう依頼→エラー→修正

  10. ↑に対応してノード配置コードの修正を依頼→修正

  11. XMLを外部ファイルとして出力するよう依頼→エラー→修正

  12. 外部ファイルのXMLをインポートできるよう依頼→修正

実際にはエラーからの修正は何度も行った場合もあります。
ですがおおむねトータル2時間ほどでコードが完成しました。

上を見れば「そんなにエラー出すの?」と思われるかもしれませんが、エラーの修正に関してはそれほど手間にはなっていません。
ものによっては苦戦しましたが、エラーを伝えるだけで直るものや、そもそも伝え方や設計の問題もあったので、そこは二人三脚試行錯誤という感じでした。
まだChatGPTにどのように書かせれば最適かを悩む部分もあったので、その辺でも少し手間取ったところでもあります。

そして完成!

そんなこんなで完成したコードはこちらです。

# ノード一覧をXMLで出力する
import bpy
import xml.etree.ElementTree as ET
import os

def socket_value_string(socket):
    if hasattr(socket, 'default_value'):
        if isinstance(socket.default_value, float) or isinstance(socket.default_value, int):
            return str(socket.default_value)
        elif isinstance(socket.default_value, tuple):
            return ','.join(map(str, socket.default_value))
    return ""

def export_material_to_xml(material):
    root = ET.Element("material")

    nodes_elem = ET.SubElement(root, "nodes")
    for node in material.node_tree.nodes:
        node_elem = ET.SubElement(nodes_elem, "node")
        node_elem.set("type", node.bl_idname)
        node_elem.set("name", node.name)
        node_elem.set("location", f"{node.location.x},{node.location.y}")

        for input in node.inputs:
            input_elem = ET.SubElement(node_elem, "input")
            input_elem.set("name", input.name)
            input_elem.set("value", socket_value_string(input))

    links_elem = ET.SubElement(root, "links")
    for link in material.node_tree.links:
        link_elem = ET.SubElement(links_elem, "link")
        link_elem.set("from_node", link.from_node.name)
        link_elem.set("from_socket", link.from_socket.name)
        link_elem.set("to_node", link.to_node.name)
        link_elem.set("to_socket", link.to_socket.name)

    return ET.tostring(root, encoding="unicode")

def save_xml_to_file(xml_string, filename, folder="data"):
    filepath = bpy.path.abspath("//")
    data_folder = os.path.join(filepath, folder)
    os.makedirs(data_folder, exist_ok=True)

    with open(os.path.join(data_folder, filename), "w", encoding="utf-8") as file:
        file.write(xml_string)

material = bpy.context.active_object.active_material
xml_string = export_material_to_xml(material)

save_xml_to_file(xml_string, "material.xml")

# XMLからノードを作成する
import bpy
import xml.etree.ElementTree as ET
import os

def socket_value_tuple(value_string, socket_type):
    if "NodeSocketVector" in socket_type or "NodeSocketColor" in socket_type:
        return tuple(map(float, value_string.split(',')))
    elif "NodeSocketFloat" in socket_type or "NodeSocketInt" in socket_type:
        return float(value_string)
    else:
        return None

def import_xml_to_material(xml_string, material):
    root = ET.fromstring(xml_string)

    nodes = root.find("nodes")
    links = root.find("links")

    node_mapping = {}
    for node_elem in nodes:
        node_type = node_elem.attrib["type"]
        node_name = node_elem.attrib["name"]
        node_location = tuple(map(float, node_elem.attrib["location"].split(',')))

        node = material.node_tree.nodes.new(node_type)
        node.name = node_name
        node.location = node_location
        node_mapping[node_name] = node

        for input_elem in node_elem.iter("input"):
            input_name = input_elem.attrib["name"]
            input_value = input_elem.attrib["value"]
            input_socket = node.inputs[input_name]

            if input_value and hasattr(input_socket, 'default_value'):
                input_socket.default_value = socket_value_tuple(input_value, input_socket.bl_idname)

    for link_elem in links:
        from_node_name = link_elem.attrib["from_node"]
        from_socket_name = link_elem.attrib["from_socket"]
        to_node_name = link_elem.attrib["to_node"]
        to_socket_name = link_elem.attrib["to_socket"]

        from_node = node_mapping[from_node_name]
        from_socket = from_node.outputs[from_socket_name]

        to_node = node_mapping[to_node_name]
        to_socket = to_node.inputs[to_socket_name]

        material.node_tree.links.new(from_socket, to_socket)

def load_xml_from_file(filename, folder="data"):
    filepath = bpy.path.abspath("//")
    data_folder = os.path.join(filepath, folder)

    with open(os.path.join(data_folder, filename), "r", encoding="utf-8") as file:
        xml_string = file.read()
    return xml_string

xml_string = load_xml_from_file("material.xml")

material = bpy.context.active_object.active_material
import_xml_to_material(xml_string, material)


ちなみに今回このコードを実行して入出力したノードはこれです。

入出力に使ったノードツリー

このノードツリーはXML形式でエクスポートされ、そのXMLファイルを読み込むことで、ノードの配置やリンクがきちんと復元されています。

もちろんこのソースコードは完璧なものではありません。
たぶんノードによっては再びエラーが出る可能性もあるし、そもそもfloatの値は復元できていてもベクトルの値などは復元できていません。
ただそれに関しては今回対応するつもりのなかったものなので、特にChatGPTの問題ではありません。

ここまで実現できれば、今後ChatGPTとの対話を通じて機能をさらに拡張していくことが可能と思いますが、今回は一旦ここまでとします。

ChatGPTにコードを書いてもらうことでわかった感動と課題、テクニック

感動1・速さ

たぶんChatGPTを使ったことがある人が特に感じるのは恐るべきその速さです。
もし仮にこのコードを自分で書こうとしたなら、何十回とググったかわかりません。更にBlenderのオートコンプリートを駆使して必要な情報の取得にも時間がかかっていたでしょう。
それを要望を伝えた瞬間に出力し始めるChatGPTの速さは感動するものがあります。

感動2・正確な命名

実のところ私は根っからのプログラマーではないので、変数にしろ関数にしろこの命名はどこまでルールに沿ったものなのかはわかりません。
ただ、常日頃から変数名や関数名やクラス名をつける度に頭を抱えていた自分としては、この速度で的確な変数名を含めたコードを書いてくれるというのは、どこまでも感動ものです。

感動3・マジで動いた!

最終的にはこれにつきます。動いたんですよ、完璧に。自分の望む形で。
何しろ今回私はコード1行も書いていません。それがまず信じられない。
本当に対話ベースでプログラミングが完了してしまう。これは今までにない経験です。

課題1・同じミスを繰り返す

とはいえ実際に使ってみると、問題もいくつか発生しました。
そのうちの一つが同じミスを繰り返すことです。

実は今回途中でこんなことがありました。
今回のコードはノードに設定してある値、default_valueもXMLに出力し、ノード生成時に再び設定する、というものがあります。
この流れの際にChatGPTはエラーのあるコードを出力しました。
というのも default_value は全てのソケットにあるわけではありません。
そこに気づかなかったChatGPTはすべてのソケットから default_value を取得しようとして、エラーになってしまったのです。
しかしそのエラーをChatGPTに伝えたところ、このように答えてすぐに修正をしてくれました。

間違いを理解したChatGPT

改めて出力されたコードは問題なく動きました。

しかし不思議なことに、コード修正のやりとりを何度か繰り返すうちに、修正した部分がいつの間にか元に戻ってしまっていたのです。

再び同じようにエラーが出たことを伝えるとChatGPTは同じような返信をし、再びコードは問題なく機能しました。

これは別のシーンでも同じような事があり、一度修正した場所が唐突に「忘れてた!」と言わんばかりに戻ってしまうことがあります。

課題2・少しずつおかしなことを言いだす

これはかなり終盤のことですが、先ほどの課題1の挙動が何度か発生したあたりから、徐々にChatGPTの回答が、正確性を欠いたものになってきました。
それまで順調にいっていたのですが一つのミスを指摘した時に、宣言してない関数内の修正を求めてきたり、今まで日本語で打っていたコメントが英語になるなどします。
そして次に出力されたコードは、今までのコード全て忘れてしまったかのような全く新しいコードでした。
もちろん大きく間違ってもいます。

さすがにこのままじゃ続けられないと思い、新しいチャットを開始しました。
それまでのやり取りの中で完成したコードをChatGTPに渡します。
工程も残り少なかったこともあり、問題なくコードは完成しましたが、少し焦りを感じる事態でした。

課題3・さすがに知識ゼロは無理

よくChatGTP関連の記事で「コードが書けない人でもプログラミングができる」とありますが、率直な印象としてはさすがに無理です。

私は一応Pythonの書き方を多少は知っているし、今回のコードも自分で書こうと思えば書けるぐらいの知識はあります。
なので出力されたコードに違和感があればすぐに気づけるし、どうやって作ればいいのかも大体分かるので方向性を指示することもできます。

結論としては関数の中身を作ってもらうことはできるけど全体の設計をするのは難しいという印象です。
ただそもそも機能を作りたいと思っている段階で、ある程度自分で設計するしかない部分はあるので、多少の知識があればかなり強力なサポートツールになってもらえます。

テクニック1・段階的に作ってもらう

ChatGTPは時に、人間のような挙動をすることがあります。
長いソースコードを一発で書かせるとミスをしやすくなりますが、ある程度段階的にかかせるとミスが少なくなります。
人間も同様でチェックなくいきなり長いコードを書くとエラーが大量に発生しますが、ある程度書いたら実行する、また次を書く、と一歩一歩進むようにコードを変えていくとミスが少なくなります。

そういう意味では普段自分が書く時と同じ段階を踏んで書かせた方が、ChatGPTは優秀になります。
これはさっきの課題3と繋がる部分であり、ゼロ知識だとこの段階でエラーの多いコードが出力されてしまいます。

テクニック2・適度に新しい会話を始める

課題2にもあったように長くやりとりを続けているうちにChatGPTは少しずつとおかしなことを言い始めます。
すると、エラーも増えて効率が低下し、コードも混乱していきます。
これを回避するためにも、テクニック1を活用し、ある程度段階的に作りつつ適度に新しい会話を始めるとよいでしょう。
その際には最終コードを次の子に渡し、コードの内容とやりたいことを理解してもらうところから始めましょう。

テクニック3・どうしても、もう少しこの子と会話を続けたいときのコツ

本当は適度に新しい会話を始めると良いですが、会話の流れで「もう少しこの対話を続けていきたい」というタイミングもあるでしょう。
そこで私が実際に効果のあったやり取りがこちらです。

ChatGPTに落ち着いてもらうためのおまじない

実はこの言葉をかける前、かなり連続してエラーのあるコードを返してきました。
やり取りもだいぶおぼつかなくなり、それでもなんとか完成まで持って行きたかった私は先ほどの言葉をかけました。
するとChatGPTはエラーのないコードを返してきたのです。

これはだいぶ驚きでしたが、このおまじないはそれほど長く続きません。
この段階まで行ってしまったらすぐにでも新しい会話を始める準備を進める方が適切です。

総括・使い方によってはかなりの時短になる強力なサポートツール

これはリアルでも言えることですが、特定の分野の話をする時にその分野についての知識が少しでもあるか、まったくないかで、伝わりやすさが大きく変わります。
それはChatGPTとの対話でも同じで、多少の知識があれば的確に伝えられるものも全く知識がなければ意思を伝えるのは困難です。
人間同士の間ではトラブルの原因ともなる事態ですが、ChatGPTはなんとしてでもこちらの意思を組もうとしてきます。
そしてノイズとなる情報が多いほど、課題2で述べたように、ChatGPTとの会話が成立しなくなっていきます。
基本的に促さなければ質問をしてくるということもないので、かなり強引なことでも実行しようとしてしまうという印象です。

けれどChatGPTが得意なこと、苦手なことが少しずつでも分かってくると、やり取りは次第にスムーズになります。
ChatGPTはコミュニケーション能力が試されるというのは、うなずけるものがあります。
けれど理解することができれば様々な面で自分をサポートしてくれる優秀な相棒になってくれます。

今後も困ったことがあればChatGPTに相談していくつもりです。
AIのこれからの進化がものすごく楽しみです。

ここまで読んでいただきありがとうございました。

おまけ:ChatGPTのブログ添削

実はこのブログ記事はChatGPTに添削を依頼しています。
さすがに一から書かせているわけではなく、全部自分で書いたものです。
(…が、いっそChatGPTに全部書かせた方が読みやすい文章になるのでは?と思わなくもない)

そこで今回依頼したのは次のような内容です。

ブログの添削を依頼

結果的には20か所ほどの修正がありましたが、さすがに添削だけの依頼なので文章の大きな部分には触れないでくれています。
以下は修正箇所の一例です。

ChatGPTからの添削

このような指摘がありました。
いや…こうやって指摘されると全く持ってその通り。
けれど大きく書き直すわけではなく、あくまで依頼した通りに細かすぎない程度の修正をしてくれました。

正直これに関しても、予想以上の結果です。
なのでちょっと恥ずかしいですが、このようにちょっとだけ公開することにしました。

別にすべてを鵜呑みにして修正する必要はなく、この記事に関してもそのままにしておきたいところは指摘があってもそのままにしてあります。
人に依頼すると「え?なんで直さなかったの?」って思われないかな…と、ちょっと顔色を窺ってしまうところもChatGPTなら気軽に自分の意見を通せます。

もう手放せなくなってる気がするChatGPT。さらなる活用法を模索していきたいと思います。

(なお、このおまけは添削依頼していません(笑))

この記事が参加している募集

よろしければサポートをお願いします。 いただいたサポートはクリエイターとしての活動資金として使わせていただきます。