見出し画像

【3D×LLMメモ1】Blenderの操作をLLMに手伝ってもらう(VRMAの修正)

VRMAをLLMが出力したスクリプトを使って、Blenderで修正することができたので、その手順を紹介する。

今回は指の修正しただけなので、全身の修正などを行う場合は、調整に時間がかかると思われる。

Blenderの環境構築

Blenderをダウンロードする。(できればv4.2.0以上)

アドオンをダウンロードするため、以下のページを表示する。

以下のようなページが表示される。

少し画面をスクロールし、「Get Add-on」をクリックする。

「+ Drag and Drop Into Blender」と表示されるので、「download」のリンクをクリックする。

以下のようなzipファイルがダウンロードされる。

Blenderを起動し、「Edit > Preferences…」をクリックする。

以下のような画面が表示されるので、右上の下三角をクリックし、「Install from Disk…」をクリックする。

先ほどダウンロードしたzipファイルを選択し、「Install from Disk」をクリックする。


BlenderにVRM(VRoid)をインポート

Blenderを起動したら、「FIle > Import > VRM(.vrm)」をクリックする。

以下の画面が表示されたら、対象のVRMファイルを選択し、「Import VRM」をクリックする。

BlenderにVRoidのモデルが表示される。

VRMA作成

以下のような手順で、VRMAを作成する。

上記の手順と作成方法は異なるが、今回は以下のようなポケダンスの指だ伸びてないようなVRMAを用意。


BlenderにVRMA(VRMアニメーション)をインポート

BlenderにVRMが既にインポートされている前提で、

対象のモーション選択し、「Import VRM Animation」をクリックする。

デフォルトのポーズからVRMAの初期値のポーズに変わっていることが分かる。

LLMでBlenderのスクリプト作成

Claudeの画面を表示する。

Blender上にインポート済みのモデルとモーションの情報をLLMに渡すため、以下のようなプロンプトを入力する。

BlenderにVRMとモーションをインポートしています。
指だけ修正したいです。
指だけ修正するスクリプトを作成するのに必要な情報を取得するスクリプトを作成してください。
import bpy

def get_finger_bones(armature):
    finger_bones = []
    finger_keywords = ['finger', 'thumb', 'index', 'middle', 'ring', 'pinky']
    
    for bone in armature.data.bones:
        if any(keyword in bone.name.lower() for keyword in finger_keywords):
            finger_bones.append(bone.name)
    
    return finger_bones

def get_finger_bone_info():
    for obj in bpy.data.objects:
        if obj.type == 'ARMATURE':
            print(f"アーマチュア名: {obj.name}")
            finger_bones = get_finger_bones(obj)
            
            print("指のボーン:")
            for bone_name in finger_bones:
                bone = obj.pose.bones[bone_name]
                print(f"  ボーン名: {bone_name}")
                print(f"    位置: {bone.head}")
                print(f"    回転: {bone.rotation_quaternion}")
                
            print(f"合計指ボーン数: {len(finger_bones)}")
            
            # アニメーションデータの確認
            if obj.animation_data and obj.animation_data.action:
                action = obj.animation_data.action
                print(f"アニメーション名: {action.name}")
                print("指のボーンに関連するFCurves:")
                for fcurve in action.fcurves:
                    if any(bone_name in fcurve.data_path for bone_name in finger_bones):
                        print(f"  データパス: {fcurve.data_path}, インデックス: {fcurve.array_index}")

# スクリプトの実行
get_finger_bone_info()

環境によるかもですが、Blenderはデフォルトだとログが確認しづらいので、参照しやすいフォルダを指定する。

結果は以下に出力するようにしてください。
C:\XXXXXXX\XXXXXXX\log
import bpy
import os
from datetime import datetime

OUTPUT_PATH = r"C:\XXXXXXX\XXXXXXX\log"

def get_finger_bones(armature):
    finger_bones = []
    finger_keywords = ['finger', 'thumb', 'index', 'middle', 'ring', 'pinky']
    
    for bone in armature.data.bones:
        if any(keyword in bone.name.lower() for keyword in finger_keywords):
            finger_bones.append(bone.name)
    
    return finger_bones

def get_finger_bone_info():
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    filename = f"finger_bone_info_{timestamp}.txt"
    filepath = os.path.join(OUTPUT_PATH, filename)
    
    with open(filepath, 'w', encoding='utf-8') as f:
        for obj in bpy.data.objects:
            if obj.type == 'ARMATURE':
                f.write(f"アーマチュア名: {obj.name}\n")
                finger_bones = get_finger_bones(obj)
                
                f.write("指のボーン:\n")
                for bone_name in finger_bones:
                    bone = obj.pose.bones[bone_name]
                    f.write(f"  ボーン名: {bone_name}\n")
                    f.write(f"    位置: {bone.head}\n")
                    f.write(f"    回転: {bone.rotation_quaternion}\n")
                
                f.write(f"合計指ボーン数: {len(finger_bones)}\n")
                
                # アニメーションデータの確認
                if obj.animation_data and obj.animation_data.action:
                    action = obj.animation_data.action
                    f.write(f"アニメーション名: {action.name}\n")
                    f.write("指のボーンに関連するFCurves:\n")
                    for fcurve in action.fcurves:
                        if any(bone_name in fcurve.data_path for bone_name in finger_bones):
                            f.write(f"  データパス: {fcurve.data_path}, インデックス: {fcurve.array_index}\n")
    
    print(f"情報が {filepath} に保存されました。")

# 出力ディレクトリの作成(存在しない場合)
os.makedirs(OUTPUT_PATH, exist_ok=True)

# スクリプトの実行
get_finger_bone_info()

スクリプトを実行するため、右上の「Scripting」をクリックする。

以下の画面が表示されるので、「New」をクリックする。

テキストエディタが起動するので、スクリプトを貼り付け、再生ボタンをクリックする。(※ログのURLは修正する必要あり)

上記のスクリプトを実行すると、指定したURLにログファイルを出力されるので、抽出した情報をプロンプトに含め、モーションの修正を依頼する。

結果は以下です。
指だけを修正するスクリプト作成してください。
指は人差し指だけを立てている状態にして、それ以外は既存のモーションまま。
---------
アーマチュア名: Armature
指のボーン:
  ボーン名: J_Bip_L_Index1
    位置: <Vector (-0.1340, -0.2972, 1.0287)>
    回転: <Quaternion (w=0.9747, x=-0.0300, y=0.0322, z=-0.2193)>
  ボーン名: J_Bip_L_Index2
    位置: <Vector (-0.1663, -0.3024, 1.0320)>
    回転: <Quaternion (w=0.8864, x=-0.0022, y=0.0119, z=-0.4627)>
  ボーン名: J_Bip_L_Index3
    位置: <Vector (-0.1804, -0.2901, 1.0243)>
    回転: <Quaternion (w=0.9986, x=0.0031, y=0.0115, z=-0.0512)>
  ボーン名: J_Bip_L_Middle1
    位置: <Vector (-0.1365, -0.3060, 1.0138)>
    回転: <Quaternion (w=0.8808, x=-0.0433, y=0.0318, z=-0.4705)>
  ボーン名: J_Bip_L_Middle2
    位置: <Vector (-0.1710, -0.2953, 1.0075)>
    回転: <Quaternion (w=0.9206, x=-0.0000, y=0.0000, z=-0.3905)>
  ボーン名: J_Bip_L_Middle3
    位置: <Vector (-0.1802, -0.2778, 0.9966)>
    回転: <Quaternion (w=0.9803, x=-0.0000, y=-0.0000, z=-0.1977)>
  ボーン名: J_Bip_L_Ring1
    位置: <Vector (-0.1375, -0.3132, 1.0001)>
    回転: <Quaternion (w=0.8801, x=-0.0097, y=-0.0161, z=-0.4745)>
  ボーン名: J_Bip_L_Ring2
    位置: <Vector (-0.1692, -0.3012, 0.9971)>
    回転: <Quaternion (w=0.9172, x=-0.0000, y=-0.0000, z=-0.3984)>
  ボーン名: J_Bip_L_Ring3
    位置: <Vector (-0.1766, -0.2847, 0.9894)>
    回転: <Quaternion (w=0.9673, x=-0.0000, y=0.0058, z=-0.2536)>
  ボーン名: J_Bip_L_Thumb1
    位置: <Vector (-0.0950, -0.2574, 0.9977)>
    回転: <Quaternion (w=0.9871, x=0.1482, y=-0.0516, z=-0.0303)>
  ボーン名: J_Bip_L_Thumb2
    位置: <Vector (-0.1296, -0.2506, 1.0274)>
    回転: <Quaternion (w=0.9939, x=0.0115, y=-0.0066, z=-0.1098)>
  ボーン名: J_Bip_L_Thumb3
    位置: <Vector (-0.1544, -0.2513, 1.0412)>
    回転: <Quaternion (w=0.9982, x=0.0051, y=-0.0080, z=-0.0596)>
  ボーン名: J_Bip_R_Index1
    位置: <Vector (-0.3014, -0.1756, 0.9868)>
    回転: <Quaternion (w=0.9986, x=-0.0411, y=-0.0203, z=0.0269)>
  ボーン名: J_Bip_R_Index2
    位置: <Vector (-0.3224, -0.2006, 0.9913)>
    回転: <Quaternion (w=0.8862, x=-0.0022, y=-0.0119, z=0.4632)>
  ボーン名: J_Bip_R_Index3
    位置: <Vector (-0.3166, -0.2200, 0.9915)>
    回転: <Quaternion (w=0.9982, x=0.0030, y=-0.0114, z=0.0581)>
  ボーン名: J_Bip_R_Middle1
    位置: <Vector (-0.3048, -0.1793, 0.9700)>
    回転: <Quaternion (w=0.9168, x=-0.0985, y=-0.0449, z=0.3844)>
  ボーン名: J_Bip_R_Middle2
    位置: <Vector (-0.3040, -0.2159, 0.9684)>
    回転: <Quaternion (w=0.8989, x=-0.0000, y=-0.0000, z=0.4381)>
  ボーン名: J_Bip_R_Middle3
    位置: <Vector (-0.2862, -0.2293, 0.9647)>
    回転: <Quaternion (w=0.9707, x=-0.0000, y=0.0000, z=0.2404)>
  ボーン名: J_Bip_R_Ring1
    位置: <Vector (-0.3068, -0.1815, 0.9548)>
    回転: <Quaternion (w=0.8754, x=-0.0348, y=0.0088, z=0.4821)>
  ボーン名: J_Bip_R_Ring2
    位置: <Vector (-0.2981, -0.2142, 0.9575)>
    回転: <Quaternion (w=0.9153, x=-0.0000, y=0.0000, z=0.4027)>
  ボーン名: J_Bip_R_Ring3
    位置: <Vector (-0.2808, -0.2235, 0.9563)>
    回転: <Quaternion (w=0.9617, x=-0.0000, y=-0.0063, z=0.2742)>
  ボーン名: J_Bip_R_Thumb1
    位置: <Vector (-0.2490, -0.1442, 0.9686)>
    回転: <Quaternion (w=0.9450, x=0.2025, y=0.0227, z=0.2559)>
  ボーン名: J_Bip_R_Thumb2
    位置: <Vector (-0.2637, -0.1852, 0.9837)>
    回転: <Quaternion (w=0.9883, x=0.0164, y=0.0066, z=0.1516)>
  ボーン名: J_Bip_R_Thumb3
    位置: <Vector (-0.2754, -0.2110, 0.9836)>
    回転: <Quaternion (w=0.9946, x=0.0102, y=0.0091, z=0.1031)>
合計指ボーン数: 24
アニメーション名: Humanoid
指のボーンに関連するFCurves:
  データパス: pose.bones["J_Bip_L_Thumb1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Thumb1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Thumb1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Thumb1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Thumb2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Thumb2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Thumb2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Thumb2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Thumb3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Thumb3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Thumb3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Thumb3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Index1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Index1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Index1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Index1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Index2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Index2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Index2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Index2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Index3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Index3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Index3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Index3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Middle1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Middle1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Middle1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Middle1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Middle2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Middle2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Middle2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Middle2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Middle3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Middle3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Middle3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Middle3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Ring1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Ring1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Ring1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Ring1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Ring2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Ring2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Ring2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Ring2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_L_Ring3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_L_Ring3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_L_Ring3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_L_Ring3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Thumb1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Thumb1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Thumb1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Thumb1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Thumb2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Thumb2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Thumb2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Thumb2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Thumb3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Thumb3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Thumb3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Thumb3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Index1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Index1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Index1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Index1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Index2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Index2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Index2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Index2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Index3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Index3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Index3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Index3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Middle1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Middle1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Middle1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Middle1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Middle2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Middle2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Middle2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Middle2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Middle3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Middle3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Middle3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Middle3"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Ring1"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Ring1"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Ring1"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Ring1"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Ring2"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Ring2"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Ring2"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Ring2"].rotation_quaternion, インデックス: 3
  データパス: pose.bones["J_Bip_R_Ring3"].rotation_quaternion, インデックス: 0
  データパス: pose.bones["J_Bip_R_Ring3"].rotation_quaternion, インデックス: 1
  データパス: pose.bones["J_Bip_R_Ring3"].rotation_quaternion, インデックス: 2
  データパス: pose.bones["J_Bip_R_Ring3"].rotation_quaternion, インデックス: 3

出力されたスクリプトを再度貼り付け、再生ボタンをクリックする。

import bpy
import math

def set_finger_rotation(bone, w, x, y, z):
    bone.rotation_quaternion = (w, x, y, z)

def modify_index_fingers():
    armature = bpy.data.objects["Armature"]
    action = armature.animation_data.action

    # 人差し指を伸ばす回転値(この値は調整が必要かもしれません)
    straight_rotation = (1, 0, 0, 0)  # w, x, y, z

    # 左右の人差し指のボーン名
    index_bones = [
        "J_Bip_L_Index1", "J_Bip_L_Index2", "J_Bip_L_Index3",
        "J_Bip_R_Index1", "J_Bip_R_Index2", "J_Bip_R_Index3"
    ]

    # アニメーションの開始フレームと終了フレーム
    start_frame = int(action.frame_range[0])
    end_frame = int(action.frame_range[1])

    for frame in range(start_frame, end_frame + 1):
        bpy.context.scene.frame_set(frame)
        
        for bone_name in index_bones:
            bone = armature.pose.bones[bone_name]
            set_finger_rotation(bone, *straight_rotation)
            
            # キーフレームを挿入
            bone.keyframe_insert(data_path="rotation_quaternion", frame=frame)

    print("人差し指のモーションを修正しました。")

# スクリプトを実行
modify_index_fingers()

結果

スクリプトが完了すると、人差し指が修正されている。

「Layout」タブの下部にある再生ボタンでも、指が修正された状態で、モーションが実行できていることが分かる。

この記事が気に入ったらサポートをしてみませんか?