見出し画像

【ChatGPT】Blender自動化するPythonのコード作成にチャレンジ

はじめに申し上げておくとこれは誰かの役に立つようなコードではなく、あくまでChatGPTを活用してはじめてPythonにチャレンジした話なので、勉強法のひとつとしてご参考までに。

きっかけ

LoRAの教師データをどう準備するかという課題があったのですが、自分はVRMのモデルでポーズをとってスクショ、またはソフトからPNGやJPGを書き出しという手順を繰り返しやっていたのですが、ここをなんとか効率化できないかなと、いろいろ探っていたことがきっかけ。

さまざまな便利なソフトを活用してましたが、blenderでもできないかと、いつも参考にさせてもらっているyonaoshiさんのこの動画を見て、んーこれはblenderで自動化できないかなと思い、Pythonを一度も勉強したことないのですが、結果として2つほど試したことになりました。

(1)jsonを読み込んでVRMのモデルにポーズを反映させる
(2)VRMモデルをランダムなカメラ位置から自動で撮影をする

試したことになりましたというのは、何もわからないのでいろいろ聞きながら進めていたところ、大きく2つのコード作成をしているんだなと途中で気づきました。実現したいこととはいえ、初心者にはハードルが高そうですがモチベーションは維持できたと思います。

現在のやり方

VRMデータでポーズを取って画像で書き出す方法はいくつかありますが、Vroid studio、VRM Porsing Desktop(Steam)、VRMお人形遊びPC版、VRMお手軽ポーズ、などが扱いやすいかなと思っています。自分でポーズも変更できますし、用意されたポーズにしてカメラ位置、画角などをきめたり、背景なども読み込みできるのでかなり自由度が高く、あるいみVRMモデルのための撮影スタジオのように使えてとても便利です。

VRMお人形遊びPC版から書き出し

もちろんこれらでも十分に効率的で便利なものなのですが、自分のようにいくつもモデルを試したい、パターンを用意したいとなると、結構手間がかかってしまいます。もっと効率化できないかなと思い、BlenderのPythonコンソールで自動化できたらいいなと思いChatGPTに投げてみたところ、なんとかなりそうかも?と思いチャレンジしてみました。

自動化してやりたいこと

VRM(3Dモデル)ファイルとjson(ポーズ)ファイルを用意して、何枚の画像がほしいか設定したら、あっというまに、ランダムなポーズで撮影されたキャラクター画像ができあがる、というものを想定していました。

(1)VRMファイルを指定したフォルダから読み込む
(2)jsonファイルを指定したフォルダからランダムに読み込む
(3)jsonファイルの値をVRMモデルのポーズに反映させる
(4)カメラをランダムな位置に配置する
(5)カメラをVRMモデルに向ける
(6)VRMモデルを撮影する
(7)指定した画像サイズで指定したフォルダに保存する
(8)2〜7を指定した数だけ繰り返す

上記の(1)〜(8)というのは、はじめから計画していたことではなく、ChatGPTとやりとりを進める中でわかってきたフローをざっくりまとめたものです。

当たり前のことなのですが、実際のアプリケーションでどのような手順で操作するか、これをコード化するわけですから、そもそもこれをしらないとChatGPTのやっていることを理解することができないため、最低限アウトプットに対する知識は持っておいたほうが良いというわけです。

ChatGPTとのやりとり

何もわからなかったのでとりあえずこんな感じかなと思うことを投げてみたところ、すぐにコードのサンプルを出してくれました。すごい。

すぐにサンプルコードを提示

早速パスなどを指定してblenderのPythonコンソールで実行してみたところエラーが出たのでエラー内容を報告。そうするとその修正したコードがすぐに出てきたので、それをまたblender内のPythonコンソールで実行、エラーの報告、実行、報告、、、、を、おそらく50回は繰り返したんじゃないかと思います、、。そう簡単にはいかないのはわかっていました。

何度も同じエラーが出て、何度も同じ回答が出てくることに苛立ちも、、

途中で(1)〜(8)のフローを一気にやるのは無理と思い、まずはjsonから読み込んだ値でポーズを変更させるということに専念。次にカメラをランダムに設置して撮影という2つのコードにわけて取り掛かることにしました。
ポーズ用のjsonは、VRM用のソフトから書き出しできるものから拝借したのですが、並びや書き方がさまざまで解読するのに時間もかかってしまうという始末。モデルのボーンの配列とjson内のキーの配列を再度humanoidの規格にあわせた並びに変更したほうがよい(本当?)など紆余曲折し、ときには質問に対して、全く検討違いな回答が繰り返されるということが多くありました。

大人気ないイライラが、、そんなつもりはなかったがAIに謝られた
いくつか質問を繰り返すうちに、前のことを忘れていたなんてことも
初心者ながらもコードを見返し、デバッグしながら原因を見つけられるように
本当にいる人とのやりとりに感じるのは気のせいか、、

成果

恥ずかしいですがコードをさらしておきます。以下の(1)と(2)を組み合わせることで、 LoRA用の教師データを一瞬で作成することを考えてましたが、結果惨敗。地道にスクショ撮ったほうが早い!ということはわかったのですが、なにかプログラムの学習方法という意味では、まるでマンツーマンでつきっきりのセンセがいたようで、少しは成果があったかなと、、。

(1)jsonを読み込んでVRMのモデルにポーズを反映させる

import bpy
import json
from mathutils import Quaternion

# VRMモデルを読み込む
bpy.ops.import_scene.vrm(filepath="VRMのパス")

# JSONファイルを読み込む
with open("jsonのパス", "r") as f:
    pose_data = json.load(f)

# シーンを更新
scene = bpy.context.scene

# 読み込んだVRMアーマチュアを取得する
vrm_armature = bpy.context.scene.objects['Armature']

# アーマチュアをアクティブにする
bpy.context.view_layer.objects.active = vrm_armature

# アクティブなオブジェクトがアーマチュアであるかどうかを確認する
if bpy.context.active_object.type == 'ARMATURE':
    print('Armature is active')
else:
    print('Armature is not active')

# ポーズモードに切り替える
bpy.ops.object.mode_set(mode='POSE')

# アーマチュアのポーズを初期化
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.transforms_clear()

# 初期ポーズのQuaternionを取得する
armature_pose_quat_data = {}
for bone in vrm_armature.pose.bones:
    armature_pose_quat_data[bone.name] = bone.rotation_quaternion

# オブジェクトモードに切り替える
bpy.ops.object.mode_set(mode='OBJECT')
    
# アーマチュアのポーズ変更前の情報を表示する
for bone in vrm_armature.pose.bones:
        
# VRMモデル内のすべてのポーズボーンとその名前を辞書に保存する
pose_bone_table = {}
for bone in vrm_armature.data.bones:
    pose_bone_table[bone.name] = vrm_armature.pose.bones[bone.name]

# 辞書の内容を表示する
for name, bone in pose_bone_table.items():

# JSONファイル内のキーとVRMのポーズボーンを自動的に対応付ける
pose_bone_table = {}
for bone in vrm_armature.data.bones:
    for pose_bone in vrm_armature.pose.bones:
        if pose_bone.bone.name == bone.name:
            pose_bone_table[bone.name] = pose_bone
            break

for bone_name, bone_data in pose_data.items():
    rotation = [float(x) for x in bone_data['rotation']]
    pose_data[bone_name]['rotation'] = rotation
    pose_data[bone_name]['quaternion'] = Quaternion(rotation)

# ポーズモードに切り替える
bpy.ops.object.mode_set(mode='POSE')

# ボーンのポーズを変更する
for bone_name, pose_quat_data in pose_data.items():
    pose_bone = pose_bone_table.get(bone_name)
    if pose_bone:
        # Quaternionコンストラクタでエラーが起きる場合があるので、例外処理を追加する
        try:
            pose_quat = Quaternion(pose_quat_data['quaternion'])
        except:
            print(f"Failed to create Quaternion from {pose_quat_data} for bone {bone_name}")
            continue
        # 初期ポーズと合成する
        armature_pose_quat = armature_pose_quat_data.get(bone_name)
        if armature_pose_quat:
            pose_quat = armature_pose_quat.inverted() @ pose_quat
        # ポーズを変更する
        pose_bone.rotation_quaternion = pose_quat

# オブジェクトモードに切り替える
bpy.ops.object.mode_set(mode='OBJECT')

# シーンを更新する
bpy.context.view_layer.update()
実行結果は恐ろしいことに!!惨敗、、

なんとかjsonの値をボーンに反映はできたものの、ボーンと値がずれていた関係で、とんでもない形に、、。VRMの基礎知識が足りないこともありこちらは一旦断念。

(2)VRMモデルをランダムなカメラ位置から自動で撮影をする

import bpy
import mathutils
import math
import random
import re
import os
import json
from mathutils import Vector, Quaternion


# VRMをインポート
bpy.ops.import_scene.vrm(filepath="VRMファイルのパス")

# ポーズ用json格納場所
json_path = "jsonのフォルダのパス"
poses = os.listdir(json_path)

# シーンの設定
scene = bpy.context.scene
scene.render.engine = 'BLENDER_EEVEE'
scene.render.image_settings.file_format = 'PNG'

# VRMオブジェクトの取得
vrm_object = bpy.context.view_layer.objects.active

# VRMオブジェクトの境界ボックスを取得
bbox = vrm_object.bound_box

# VRMオブジェクトの境界ボックスの中心を計算
bbox_center = sum((Vector(b) for b in bbox), Vector()) / 8

# VRMオブジェクトのバウンディングボックスの対角線の長さを計算
bbox = vrm_object.bound_box
bbox_center = sum((mathutils.Vector(p) for p in bbox), mathutils.Vector()) / 8
bbox_diag = (max(bbox, key=lambda p: p[0])[0] - min(bbox, key=lambda p: p[0])[0])**2 \
          + (max(bbox, key=lambda p: p[1])[1] - min(bbox, key=lambda p: p[1])[1])**2 \
          + (max(bbox, key=lambda p: p[2])[2] - min(bbox, key=lambda p: p[2])[2])**2
bbox_diag = bbox_diag**0.5

# カメラの設定
camera = bpy.data.objects['Camera']

# VRMオブジェクトを選択
vrm_object.select_set(True)

# アーマチュアをアクティブにする
for obj in bpy.context.scene.objects:
    if obj.type == 'ARMATURE' and obj.name == 'Armature':
        bpy.context.view_layer.objects.active = obj
        break
		
# アクティブなアーマチュアを取得する
obj = bpy.context.view_layer.objects.active
if obj and obj.type == 'ARMATURE':
    armature = obj.data
    bpy.context.view_layer.objects.active = obj
    bpy.ops.object.mode_set(mode='POSE') # ポーズモードに切り替える
    if bpy.context.object.mode == 'POSE':
        print("ポーズモードに切り替わりました。")
    else:
        print("ポーズモードに切り替わりませんでした。")
    print("アーマチュアオブジェクト:", armature)
else:
    print("No armature object found.")
    armature = None
	
# 対象となる首のボーンオブジェクトを取得する
neck_bone_name = "J_Bip_C_Neck"
neck_bone_obj = None
if armature:
    neck_bone_obj = armature.bones.get(neck_bone_name)

# 対象ボーンが選択されている場合はそのボーンを対象にする
target_bone = None
if armature:
    for pbone in obj.pose.bones:
        if pbone.bone.name == neck_bone_name:
            target_bone = pbone
            break

if target_bone:
    print("選択されたボーン:", target_bone.name)
else:
    print("No bone selected.")


# 対象となる首の位置を取得する
obj = bpy.context.object
neck_bone_name = "J_Bip_C_Neck"
neck_bone_obj = obj.pose.bones.get(neck_bone_name)

if neck_bone_obj:
    neck_position_local = neck_bone_obj.head
    neck_position_global = obj.matrix_world @ neck_bone_obj.matrix @ neck_position_local
    bpy.context.scene.cursor.location = neck_position_global
    print("ローカル座標系での首の位置:", neck_position_local)
    print("グローバル座標系での首の位置:", neck_position_global)
else:
    print("Could not find neck bone: {}".format(neck_bone_name))


# 首のボーンが見つかった場合、3Dカーソルを移動させる
if neck_bone_obj:
    cursor_location = neck_bone_obj.head
    bpy.context.scene.cursor.location = cursor_location


# 新しいカメラの名前とデータを設定
new_camera_obj_name = "NewCamera"
new_camera_data = bpy.data.cameras.new(new_camera_obj_name)

# アクティブなオブジェクトを取得
new_camera_obj = bpy.data.objects.new(new_camera_obj_name, new_camera_data)

old_camera_obj = None # 旧カメラを削除する
for obj in bpy.data.objects:
    if obj.type == 'CAMERA' and obj != new_camera_obj:
        bpy.data.objects.remove(obj, do_unlink=True)
	
# 撮影くりかえし
for i in range(10): 
    # ポーズのjsonファイルをランダムに選択
    pose_file = os.path.join(json_path, random.choice(poses))
    print("Selected pose file: {}".format(pose_file))
	# 撮影処理
    camera_numbers = set()
    for obj in bpy.data.objects:
        if obj.type == 'CAMERA':
            match = re.match(r'Camera(\d+)', obj.name)
            if match:
                camera_numbers.add(int(match.group(1)))
                print(camera_numbers)
    new_camera_number = 1
    while new_camera_number in camera_numbers:
        new_camera_number += 1
    # カメラを作成
    new_camera_data = bpy.data.cameras.new('Camera') 
    new_camera_obj_name = "Camera{:02d}".format(new_camera_number)
    new_camera_obj = bpy.data.objects.new(new_camera_obj_name, new_camera_data)
    #old_camera_obj = None # 旧カメラを削除する
    #for obj in bpy.data.objects:
    #    if obj.type == 'CAMERA' and obj != new_camera_obj:
    #        bpy.data.objects.remove(obj, do_unlink=True)
    camera_numbers.add(new_camera_number)
    x = round(random.uniform(-2.0, 2.0), 1)
    z = round(random.uniform(1.0, 3.0), 1)
    y = -2
    chosen_point = (x, y, z)
    print(chosen_point)
    # 新しいカメラの位置を指定
    new_camera_location = mathutils.Vector(chosen_point) 
    new_camera_obj.location = new_camera_location
    # シーンにカメラを追加する
    bpy.context.scene.collection.objects.link(new_camera_obj) 
    # カメラの表示設定
    bpy.context.view_layer.objects.active = new_camera_obj 
    bpy.ops.object.visual_transform_apply()
    new_camera_obj.data.display_size = 2.0
    new_camera_obj.show_name = True
    print("Camera created: {}".format(new_camera_obj.name))
    # カメラをアクティブにする
    bpy.context.view_layer.objects.active = new_camera_obj 
    print("Active:", new_camera_obj)
    bpy.ops.object.transform_apply(location=True, rotation=True, scale=True) 
    # カメラを3Dカーソルの位置に向ける
    cursor_location = bpy.context.scene.cursor.location 
    camera_location = new_camera_obj.location
    camera_direction = (cursor_location - camera_location).normalized()
    up_vector = Vector((0, 1, 0))
    camera_rotation = camera_direction.to_track_quat('-Z', 'Y')
    up_quat = camera_rotation @ up_vector.to_track_quat('-Z', 'Y').inverted()
    new_camera_obj.rotation_mode = 'QUATERNION'
    new_camera_obj.rotation_quaternion = (cursor_location - new_camera_obj.location).to_track_quat('-Z', 'Y')
    print("Cursor Location:", cursor_location)
    print("Camera Location:", camera_location)
    print("Camera Direction:", camera_direction)
    print("Camera Rotation:", camera_rotation)
    print("Up Quaternion:", up_quat)
    # カメラの距離と解像度
    camera_distance = 1 * bbox_diag 
    render = bpy.context.scene.render
    # カメラの画角の計算
    aspect_ratio = render.resolution_x / render.resolution_y 
    new_camera_data.angle = 0.5 * math.atan((bbox_diag / 1) / (camera_distance * aspect_ratio))
    # レンダリングの設定
    render = bpy.context.scene.render 
    # 画像の解像度を自動調整
    resolution_scale = max(vrm_object.dimensions) * 2.0 
    render.resolution_x = int(1024 * resolution_scale)
    render.resolution_y = int(1024 * resolution_scale)
    # レンダリング時のポストプロセスを有効にする
    render.use_compositing = True 
    # カメラが存在するか確認
    camera = bpy.data.objects.get(new_camera_obj_name) 
    if camera is None:
        print("カメラが存在しません")
    # レンダリング
    else: 
        # カメラをアクティブに設定する
        bpy.context.scene.camera = camera 
        num_frames = 1
        if bpy.context.scene.camera is None:
            bpy.context.scene.camera = camera
        for frame in range(num_frames):
            bpy.context.scene.frame_set(frame)
            render = bpy.context.scene.render
            render.filepath = "/Volumes/A002/blender/Output/frame_{}_{}.png".format(i, frame)
            bpy.ops.render.render(write_still=True, use_viewport=True, scene=bpy.context.scene.name)
            print("Rendered frame_{}_{}.png".format(i, frame))


# VRMを削除
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
ポーズ変更はおいといて、ランダムにカメラ位置を配置しVRMに向かって撮影は成功
とりあえず10枚指定して、さまざまな角度から

コードにはデバッグ用にprint文があちこちについてますが、それがわりと勉強になりました。カメラから被写体までの構図の変化のつけ方として、あとはレンズをランダムで設定するということもできそうです。

もしどなたかこれが実現できそうでしたら、、。

コミュニケーション能力とアウトプットに対する知識が問われる

ChatGPTを活用してプログラムを作成というよりは、ChatGPTと一緒に考えながら作る感覚、マンツーマンのセンセにも近かったように思います。はじめてPythonに触れた感じとしては、途中で興味が薄れることもなく勉強できたのかなと、、。

AIだから何かすごいものすぐにポンっと出してくれるということではなく、何を作りたいのか、何を回答として得たいのか、こちらからどんな情報を与えられるのか、的確な質問、指示を伝えられるか、そのあたりが問われるように思います。逆にいえば、それらが明確な人にしてみたら、ものすごい手段、パートナーを手にいれたような感じだと思います。
また、もし普通に学習しようとしてたら、何かのオンライン講座を受講して基本を、、みたいな感じで進めてたかもしれませんが、なにかとてつもなくすごい学習法のようにも感じます。

今回の失敗の原因としては自分のVRMモデルの構造に対する知識の無さが原因でした。いずれにしてもアウトプットに対する技術・知識・経験があるかないかで、ChatGPTへの情報の伝え方が変わってくると思いますので、アウトプットに関連する貪欲な興味・関心もかなり大事かなと思います。

今後は自由な発想やアイデアとともに、AIをうまく活用できる人が生き残るんだと思います。


いいなと思ったら応援しよう!