こんにちは。この新連載では、AI生成技術と進化、3DCG制作現場への活用の可能性を探索していきます。今回取り上げるのは、多くの人々を驚かせている言語モデル、OpenAI ChatGPTAutodesk Mayaの連携についてです。

AI関連の研究は日々驚くほどのスピードで進化しています。可能な限り正確な情報を提供するよう心がけていますが、私自身も学習中であるため、記事中に誤りが含まれる可能性があることをご理解いただければ幸いです。

記事の目次

    赤崎弘幸

    Jet Studio Inc.所属のCGディレクター。得意分野はキャラクターモデリング、リギング、ツール作成等。3DCGの制作現場目線でAI活用の可能性を探究中。

    1. ChatGPTのスクリプティング能力

    ChatGPT(GPT-3.5やGPT-4)は、自然言語の理解と人間との対話をシミュレートするように特化してチューニングされたモデルです。まるで人間と会話しているかのような返答文を生成する能力で注目を集めています。

    また、その力は自然言語だけでなく、プログラミング言語(Pythonなど)に対しても適用可能で、多様なタスクに対応します。特にGPT-4は入出力できる文字数が多く、より長い文章の理解と生成が可能になっています。

    では具体的にどのようなスクリプトが生成できるのでしょうか。以下は、ブラウザ版ChatGPT(GPT-4)によって生成されたMayaのスクリプトの一例です。

    ユーザー入力:

    Maya PythonでPySide2を使い、次の条件を満たすUIを作成してください。


    # ウィジェット
    * チェックボックス: ラベル="てすと", デフォルトon
    * プルダウン: ラベル="次の中から選択", アイテム=["aaa", "bbb", "ccc"]
    * テキストフィールド: ラベル="テキスト入力", デフォルト="Hello!"
    * floatフィールド: ラベル="数値", 0.0~1.0, デフォルト=0.5
    * ボタン: ラベル="OK"
    * ボタン: ラベル="キャンセル"


    # 動作
    * チェックボックス、プルダウン、テキストフィールド、floatフィールドはそれぞれ、変更されたときに関数を呼び出し辞書型の内部変数に変更後の値を格納してください。
    * OKボタンが押されたときはウィンドウを閉じると同時に、内部変数をjson形式で保存してください。保存先はwindowsのユーザーtempフォルダです。
    * キャンセルボタンを押したときは何もせずウィンドウを閉じてください。


    # 注意
    UIはMayaのメインウィンドウが親になるように記述してください。必要であればshiboken2.WrapInstanceを使用してください。


    GPT-4出力:

    以下に指定された要件に基づくPySide2とMaya Pythonを使用したコードスニペットを示します。このコードは、すべての指定されたウィジェットと対応する機能を作成します。
    
    from PySide2 import QtWidgets, QtCore
    from shiboken2 import wrapInstance
    import maya.OpenMayaUI as omui
    import os
    import json
    import tempfile
    
    def maya_main_window():
        main_window_ptr = omui.MQtUtil.mainWindow()
        return wrapInstance(long(main_window_ptr), QtWidgets.QWidget)
    
    class MyDialog(QtWidgets.QDialog):
        def __init__(self, parent=maya_main_window()):
            super(MyDialog, self).__init__(parent)
    
            self.setWindowTitle("UI")
            self.setMinimumWidth(200)
            self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
    
            self.data = {
                "checkbox": True,
                "dropdown": "aaa",
                "text": "Hello!",
                "float": 0.5
            }
    
            self.create_widgets()
            self.create_layout()
            self.create_connections()
    
        def create_widgets(self):
            self.checkbox = QtWidgets.QCheckBox("てすと")
            self.checkbox.setChecked(True)
    
            self.dropdown = QtWidgets.QComboBox()
            self.dropdown.addItems(["aaa", "bbb", "ccc"])
    
            self.text_field = QtWidgets.QLineEdit("Hello!")
    
            self.float_field = QtWidgets.QDoubleSpinBox()
            self.float_field.setRange(0, 1)
            self.float_field.setSingleStep(0.01)
            self.float_field.setValue(0.5)
    
            self.ok_button = QtWidgets.QPushButton("OK")
            self.cancel_button = QtWidgets.QPushButton("キャンセル")
    
        def create_layout(self):
            layout = QtWidgets.QFormLayout(self)
            layout.addRow(self.checkbox)
            layout.addRow("次の中から選択", self.dropdown)
            layout.addRow("テキスト入力", self.text_field)
            layout.addRow("数値", self.float_field)
            layout.addRow(self.ok_button, self.cancel_button)
    
        def create_connections(self):
            self.checkbox.stateChanged.connect(self.update_data)
            self.dropdown.currentTextChanged.connect(self.update_data)
            self.text_field.textChanged.connect(self.update_data)
            self.float_field.valueChanged.connect(self.update_data)
    
            self.ok_button.clicked.connect(self.save_and_close)
            self.cancel_button.clicked.connect(self.close)
    
        def update_data(self):
            self.data = {
                "checkbox": self.checkbox.isChecked(),
                "dropdown": self.dropdown.currentText(),
                "text": self.text_field.text(),
                "float": self.float_field.value()
            }
    
        def save_and_close(self):
            file_path = os.path.join(tempfile.gettempdir(), "data.json")
            with open(file_path, 'w') as f:
                json.dump(self.data, f, indent=4)
            self.close()
    
    dialog = MyDialog()
    dialog.show()
    
    このコードは、Maya PythonでPySide2を使用して、各ウィジェットが更新されたときにデータを更新し、最後にそのデータをJSONとして保存するUIを作成します。ユーザーが"OK"ボタンを押すと、このデータはユーザーの一時フォルダに保存されます。"キャンセル"ボタンを押すと、何も保存せずにUIが閉じます。

    上記の例では、成功率を上げるためある程度スクリプトの知識がある前提での指示文を書いています。

    実際に出力されたスクリプトは、Python3では10行目のwrapInstanceでエラーを吐いてしまいますが、long()をint()に直すだけであとは指示通りの挙動で正常に動作します(※Maya 2024で実行を確認)。

    ここまで詳細に指示を書かなくても、何度かやり取りを繰り返しながら修正させていくことも可能です。

    スクリプトが自動生成されるということは、日々の作業を自動化するツールを作成するのに役立ちそうです。そもそもスクリプト経験が浅い人でも、指示さえできればほぼ書けてしまう点は非常に大きいですね。

    次項ではAPI経由でGPT-3.5/4を自作ツールに組み込み、Mayaから利用できるようにしてみます。


    2. APIを利用してツールに組み込む利点

    ブラウザ版だけでも十分に便利なのですが、あえてAPIを使ってツールに組み込むメリットをいくつか考えてみます。

    ①プロンプトテンプレート
    機能ごとにプロンプトのテンプレートを用意しておけば、コピペや書き換えの手間を省くことができます。例えば「以下の要件を満たすMaya Pythonスクリプトを書いてください。・・・」のような毎回冒頭に入れるであろう定型文などです。

    ②シームレス性
    入力、Pythonコード生成、生成されたコードの実行、エラーのフィードバック等が全てMaya上で完結するため、ブラウザと行き来するよりも多少効率が上がるかもしれません。

    ③コンテキストの理解
    Maya上であればMayaのコマンドを実行できるため、シーンの状態を解析し結果をそのまま言語モデルに送ることができます。あまり複雑なものは難しそうですが、Script Editorに出力されている履歴やエラー出力は使いやすそうです。

    ④音声入力や読み上げ
    これはおまけのような位置付けですが、音声入力や読み上げアプリケーションと連携し、より直感的にアシスタントとして利用することができるかもしれません。


    3. 活用例

    前項で挙げたメリットを享受できるよう、簡単なツールをいくつか作成してみました。


    <テスト環境>

    Windows 10

    Maya 2023 (Python3.9.7)

    Maya 2024 (Python3.10.8)


    事前準備として、以下の2点が実施されているものとします。

    API keys - OpenAI APIよりAPI Keyを取得し、環境変数OPENAI_API_KEYに設定する。

    Maya ヘルプ | mayapy pip を使用して Python パッケージを管理する | Autodeskを参考にMayaopenaikeyboardパッケージをインストールする。


    cd C:\Program Files\Autodesk\Maya2024\bin
    
    mayapy -m pip install openai keyboard -t C:\Users\ユーザー名\Documents\maya\2024\scripts\site-packages

    また、APIの具体的な使用方法は以下をご確認いただければと思います。
    API Reference - OpenAI API
    openai-cookbook/How_to_format_inputs_to_ChatGPT_models.ipynb


    <活用例1>作業中に発生したエラーの原因を教えてくれるダイアログ

    単純にエラーコードをUI下部の出力部分から中ボタンドラッグして質問するだけのシンプルなものです。コードは以下の通りになります。

    from maya import cmds, OpenMayaUI
    from PySide2 import QtWidgets
    from shiboken2 import wrapInstance
    
    import keyboard
    import openai
    MODEL_NAME = "gpt-3.5-turbo"  # 使用する OpenAI モデルの名前
    
    def chat_completion_stream(messages:list, model:str=MODEL_NAME, **kwargs) -> str:
        """
        OpenAI モデルを使って、与えられたメッセージリストに対する返答を生成します。
    
        Args:
            messages (list): ユーザーと AI モデルのメッセージのリスト。
            model (str, optional): 使用する AI モデル。デフォルトは MODEL_NAME。
    
        Returns:
            str: AI モデルによる返答。
        """
        result = openai.ChatCompletion.create(
            model=model,
            messages=messages,
            stream=True,
            **kwargs
        )
        for chunk in result:  # モデルからの返答を取得します
            if chunk:
                content = chunk['choices'][0]['delta'].get('content')
                if content:
                    yield content  # 取得した内容を返します
    
    def maya_main_window():
        """
        Maya のメインウィンドウのインスタンスを取得します。
    
        Returns:
            QtWidgets.QMainWindow: Maya のメインウィンドウ。
        """
        main_window_ptr = OpenMayaUI.MQtUtil.mainWindow()
        return wrapInstance(int(main_window_ptr), QtWidgets.QMainWindow)
    
    class ErrorExpositorUI(QtWidgets.QMainWindow):
        """
        エラーメッセージを入力し、それに対する説明を生成するための GUI クラスです。
        """
        def __init__(self, parent=None):
            super(ErrorExpositorUI, self).__init__(parent)
    
            self.setWindowTitle(u'エラー解説')
            self.setGeometry(800, 300, 400, 300)
            
            # ユーザー入力欄の設定
            self.user_input = QtWidgets.QPlainTextEdit()
            self.user_input.setPlaceholderText("エラー文をここに中ボタンドラッグ")
            
            # 解説ボタンの設定
            self.explanation_button = QtWidgets.QPushButton('解説頼む!')
            self.explanation_button.clicked.connect(self.explanation)
    
            # AI の説明を表示するテキストエリアの設定
            self.text_edit = QtWidgets.QTextEdit()
            self.text_edit.setMinimumHeight(200)
            self.text_edit.setReadOnly(True)
    
            # レイアウトの設定
            main_layout = QtWidgets.QVBoxLayout(self)
            main_layout.addWidget(self.user_input)
            main_layout.addWidget(self.explanation_button)
            main_layout.addWidget(self.text_edit)
            
            widget = QtWidgets.QWidget(self)
            widget.setLayout(main_layout)
            self.setCentralWidget(widget)
    
        def explanation(self):
            """
            ユーザーが入力したエラーメッセージを取得し、AI モデルを使って説明を生成します。
            """
            error_message = self.user_input.toPlainText()
    
            # 入力が空でないことを確認します
            if not error_message.strip():
                return
    
            # プロンプトの作成
            prompt = """以下はAutodesk Mayaが出力したエラー文です。原因と解決策を分かりやすく簡潔に説明してください。
    
    # エラー:
    {}
    
    # 解説:
    """.format(error_message)
    
            messages = [{"role":"user", "content":prompt}]
    
            try:
                options = {"temperature": 0, "top_p": 1}
                message_text = ""
                # モデルからの返答を取得し、表示します
                for content in chat_completion_stream(messages=messages, model=MODEL_NAME, **options):
                    # ESC キーが押された場合は処理を終了します
                    if keyboard.is_pressed('esc'):
                        break
                    message_text += content
                    self.text_edit.setText(message_text)
                    cmds.refresh()
            except Exception as e:
                return
    
    if __name__ == "__main__":
        # Maya のメインウィンドウを親として GUI を表示します
        ui = ErrorExpositorUI(maya_main_window())
        ui.show()

    [解説頼む!]ボタンを押すとexplanationメソッドが呼ばれ、UI上部にペーストしたエラー文が読み取られます。その後、prompt変数の{}の部分に入力したエラー文が挿入され、APIに送信されます。

    このprompt変数がプロンプトのテンプレになっています。些細なことですが、前置きの指示文を打ち込む手間を省いています。

    APIのリクエスト結果はstreamモードで逐次的に結果を取得し、随時UI下部のテキストフィールドを更新します。chat_completion_streamがジェネレータ関数です。[Esc]キーを押すと中断できるようにしてあります。

    ちなみに、コード内の関数ドキュメントと各コメントはGPT-4に追記してもらったものを少し添削しています。こういった使い方もできるので非常に便利ですね。



    <活用例2>手動オペレーションをキャプチャしPythonスクリプト化するツール

    「今からやる作業を自動化したい!」といったときに、言葉で指示をするより確実な結果が得られます。そのままでは汎用的には使えませんが、ノード名などを書き換えるだけで活用できるのでベースとしては十分に使えそうです。


    具体的な処理は以下の通りです。


    ①オペレーション中にScript Editorの上部に出力されるMELコマンドをキャプチャする。

    ②Pythonに書き換える指示をAPIに送信。

    ③結果を受け取りUIに表示する。


    次にコードを示します。


    from pathlib import Path
    from uuid import uuid4
    import re
    
    from maya import cmds, OpenMayaUI
    from PySide2 import QtWidgets
    from shiboken2 import wrapInstance
    
    import keyboard
    import openai
    MODEL_NAME = "gpt-3.5-turbo"  # 使用するモデル名を定義
    
    def chat_completion_stream(messages:list, model:str=MODEL_NAME, **kwargs) -> str:
        """ OpenAIのChatCompletionを利用して、チャット形式の対話をモデルに投げ、結果をストリーム形式で受け取る関数。
        Args:
            messages (list): チャット形式の対話リスト。
            model (str): 使用するモデル名。デフォルトは"gpt-3.5-turbo"。
        Yields:
            str: モデルからの返答。
        """
        # チャット形式の対話を生成し、結果をストリーム形式で受け取ります
        result = openai.ChatCompletion.create(
            model=model,
            messages=messages,
            stream=True,
            **kwargs
        )    
        for chunk in result:
            if chunk:
                # 返答の中からcontentの部分を取り出します
                content = chunk['choices'][0]['delta'].get('content')
                if content:
                    # 取り出したcontentを返します
                    yield content
    
    def maya_main_window(*args):
        """ Mayaのメインウィンドウのインスタンスを返す関数。
        Returns:
            QtWidgets.QMainWindow: Mayaのメインウィンドウのインスタンス。
        """
        main_window_ptr = OpenMayaUI.MQtUtil.mainWindow()
        return wrapInstance(int(main_window_ptr), QtWidgets.QMainWindow)
    
    class RecorderUI(QtWidgets.QDialog):
        """ Mayaでのユーザー操作を記録し、その記録を基にスクリプトを自動生成するUI。"""
    
        def __init__(self, parent=None, *args, **kwargs):
            super(RecorderUI, self).__init__(parent)
            
            # 一時ファイルの保存先を定義します
            self.temp_dir = Path.home() / 'AppData' / 'Local' / 'Temp'  
            self.history_file = Path()  # 操作履歴の保存先ファイルを初期化します
            self._is_recording = False  # 録画状態を表すフラグを初期化します
    
            self.build_ui()  # UIを構築するメソッドを呼び出す
    
        def build_ui(self, *args):
            """ UIを構築するメソッド。 """
    
            self.setWindowTitle('Action Recorder')
            self.setGeometry(400, 400, 600, 300)
            
            self.rec_button = QtWidgets.QPushButton('Start REC')
            self.rec_button.clicked.connect(self.toggle_recording)
    
            self.script_editor = cmds.cmdScrollFieldExecuter(st="python", sln=True)
            script_editor_ptr = OpenMayaUI.MQtUtil.findControl(self.script_editor)
            script_editor_widget = wrapInstance(int(script_editor_ptr), QtWidgets.QWidget)
    
            execute_button = QtWidgets.QPushButton('Execute')
            execute_button.clicked.connect(self.execute_script)
    
            main_layout = QtWidgets.QVBoxLayout(self)
            main_layout.addWidget(self.rec_button)
            main_layout.addWidget(script_editor_widget)
            main_layout.addWidget(execute_button)
            self.setLayout(main_layout)
    
        def toggle_recording(self, *args):
            """ レコーディングの開始/停止を切り替える関数 """
    
            if self._is_recording:
                self._is_recording = False
                self.rec_button.setText('Start REC')
                self.rec_button.setStyleSheet("")
                self.stop_rec()
            else:
                self._is_recording = True
                self.rec_button.setText('Stop REC')
                self.rec_button.setStyleSheet("background-color: red")
                self.start_rec()
    
        def start_rec(self, *args):
            """ レコーディングを開始する関数 """
    
            # 履歴ファイルのパスを生成します
            self.history_file = Path(self.temp_dir, str(uuid4()) + ".txt")
            # Mayaのスクリプトエディタに対して履歴の書き込みを開始するよう指示します
            cmds.scriptEditorInfo(historyFilename=str(self.history_file), writeHistory=True)
    
        def stop_rec(self, *args):
            """ レコーディングを停止する関数 """
    
            # Mayaのスクリプトエディタに対して履歴の書き込みを停止するよう指示します
            cmds.scriptEditorInfo(writeHistory=False)
            with open(self.history_file, mode='r', encoding="utf-8") as f:
                self.history_text = f.read()
    
            if not self.history_text.strip():
                return
    
            # OpenAIに対するプロンプトを生成します
            prompt = """以下はAutodesk Mayaのスクリプト言語MELで書かれたスクリプトです。
    処理内容を変えずに、Maya Pythonに書き換えてください。
    必ず1つのコードブロックにまとめてください。
    
    # MEL:
    ```mel
    {}
    ```
    
    # Python:
    ```python
    """.format(self.history_text)
    
            messages = [{"role":"user", "content":prompt}]
            
            try:
                # OpenAIにプロンプトを送信し、結果を取得します
                options = {"temperature": 0, "top_p": 1}
                message_text = ""
                for content in chat_completion_stream(messages=messages, model=MODEL_NAME, **options):
                    if keyboard.is_pressed('esc'):
                        break
                    message_text += content
                    cmds.cmdScrollFieldExecuter(self.script_editor, e=True, text=message_text)
                    cmds.refresh()
            except Exception as e:
                return
            
            _, code_list = self.decompose_response(message_text)
            if code_list:
                cmds.cmdScrollFieldExecuter(self.script_editor, e=True, t=code_list[0])
        
        def decompose_response(self, txt:str, *args):
            """ モデルからの応答をコメントとコードに分解するメソッド。 """
    
            # レスポンスからコード部分を抽出します
            pattern = r"```python([\s\S]*?)```"
            code_list = re.findall(pattern, txt)
            code_list = [code.strip() for code in code_list]
            
            # コメント部分を抽出します
            comment = re.sub(pattern, '', txt)
            comment = re.sub('[\r?\n]+', '\n', comment)
            comment = comment.strip()
            
            return comment.strip(), code_list
    
        def execute_script(self, *args):
            """ スクリプトを実行する関数 """
            cmds.cmdScrollFieldExecuter(self.script_editor, e=True, executeAll=True)
    
    if __name__ == "__main__":
        ui = RecorderUI(maya_main_window())
        ui.show()

    [Start Rec]ボタンを押すとstart_recメソッドが呼ばれ、scriptEditorInfoコマンドによりスクリプトエディタ上部の出力結果がファイルに書き込まれるようになります。もう一度ボタンを押すと、stop_recメソッドが呼ばれファイル出力を停止します。その後出力されたtxtファイルからMELコードを含む一連の文字列を読み取ります。

    stop_recメソッドでプロンプトテンプレートにMELコードをあてはめ、まとめてAPIに送信しています。




    <活用例3>読み上げ・実行・エラーフィードバック付きのチャットUI

    処理内容は以下の通りです。

    ①UI左下にメッセージを入力し送信すると、以下のようなプロンプトとともにメッセージが送信されます。


    systemプロンプト:


    質問に対して、Maya Pythonスクリプトを作成してください。
    スクリプト以外のテキストはすべて短めに、日本語で書いてください。
    Python のコードブロックは常に```pythonで始めてください。
    Mayaに標準でインストールされていないパッケージやモジュールは使用しないでください。
    スクリプトを書くための情報が不足している場合は、適宜質問してください。

    userプロンプト:

    以下の質問に答えられるよう、Maya で実行可能なPythonスクリプトを作成してください。



    # 質問:
    {入力したメッセージ}

    ②同じく結果を逐次的に受け取りUIに表示します。これと同時に、コードブロックではない箇所でかつ「。!?」等の文字が現れたら、そこで文を区切って即座に音声合成キューに追加します。


    音声合成が終わったら今度はそのままwavファイルを音声再生キューに追加し、順番に再生します。音声合成と音声再生はconcurrent.futuresモジュールを使用して別スレッドで実行し、メイン処理が完全に止まらないようにしています。


    この例では音声合成ソフトウェアにVOICEVOXを使用しています。GUI版を立ち上げておいてもいいですし、エンジンのみを立ち上げておいても良いです。


    ③生成が終わったら、コードブロック(```pythonと```で囲まれた部分)を抽出し、UI右側のスクリプトフィールドに移します。


    ④UI右下の[Execute]ボタンを押すとスクリプトを実行します。実行時にエラーが発生した場合はそのエラー出力を保持しておき、[Fix Error]ボタンを押したときの情報として使用できるようにします。


    ⑤[Fix Error]ボタンを押すと①と同じしくみでメッセージを送信しますが、エラー修復用のプロンプトを使用する点が異なります。プロンプトは以下の通りです。


    実行したら以下のようなエラーが出ました。修復してください。



    # Error:
    {④でキャッチしたエラー文}

    この例では手動でボタンを押して修正依頼する方式にしていますが、エラーがなくなるまで処理を実行し続ければ「自動デバッグ」が実現するかもしれません。コードは以下のリポジトリに全て置いてあります。


    GitHub - akasaki1211/ChatGPT_Maya




    4. 今後の可能性

    本稿で紹介したのはシンプルな一問一答を使いやすくした程度のものでした。しかし、言語モデルのポテンシャルを考えると、DCCツールとの連携に限って考えてもさらに魅力的な可能性が広がっています。


    例えば、

    ・自然言語を介してLLMにプロシージャルモデリングのパラメータを選択させる。

    ・シーン内の情報を得るためのツールを複数用意し、ReAct方式に倣ってエージェントを作成する。

    などが考えられます。(妄想です)


    ただし、さすがにMayaなどのシーンから得られる複雑な3Dデータを理解する能力はLLMにはないので、3D特化の複雑なアプローチは期待できないかもしれません。これが追加学習などにより改善されるかどうかは、専門家でない私には未知の領域です。


    最後までお読みいただき、ありがとうございます。今回はMayaとChatGPTとの組み合わせをテーマにその可能性の一部を紹介させていただきました。次回以降も様々なAI技術と3DCGの応用について探求していきますので、お楽しみに!


    TEXT_赤崎弘幸(Jet Studio Inc.) / Hiroyuki Akasaki

    EDIT_中川裕介(CGWORLD) / Yusuke Nakagawa