見出し画像

Blenderで複数VRMAの統合・修正・出力

Blenderのスクリプトを使って、2つのVRMAを加工して、1つのVRMAと出力できたので、そのスクリプトを残しておく。

環境構築

以下の記事などを参考に、Blenderの環境構築を実施する。

スクリプト

2つのVRMAを統合する。
「BLANK_FRAMES = 30」についてはつなぎのフレームのため、不要であれば「0」にする。

import bpy

# VRMファイルのパス
vrm_file_path = r"C:\Users\XXXXX\XXXXX.vrm"

# 最初のVRMアニメーションファイルのパス
vrma_animation_path_1 = r"C:\Users\XXXXX\XXXXX.vrma"

# 追加するVRMアニメーションファイルのパス
vrma_animation_path_2 = r"C:\Users\XXXXX\XXXXX.vrma"

# モーション間の空白フレーム数
BLANK_FRAMES = 30

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

# インポートしたVRMモデルを選択
imported_objects = bpy.context.selected_objects
if imported_objects:
    vrm_model = imported_objects[0]
    bpy.context.view_layer.objects.active = vrm_model
    vrm_model.select_set(True)
else:
    print("VRMモデルが正しくインポートされませんでした。")

# アニメーションをインポートする関数
def import_animation(filepath, start_frame):
    bpy.ops.import_scene.vrma(filepath=filepath, armature_object_name="")
    
    # インポートされたアクションを取得
    action = bpy.data.actions[-1]
    
    # アクションの名前を変更(重複を避けるため)
    action.name = f"Animation_{start_frame}"
    
    # キーフレームをオフセット
    for fcurve in action.fcurves:
        for keyframe in fcurve.keyframe_points:
            keyframe.co[0] += start_frame
    
    return action

# 最初のアニメーションをインポート
action1 = import_animation(vrma_animation_path_1, 0)

# 最初のアニメーションの長さを取得
animation_length_1 = int(max(fcurve.keyframe_points[-1].co[0] for fcurve in action1.fcurves))

# 2つ目のアニメーションをインポート(空白時間を考慮)
action2 = import_animation(vrma_animation_path_2, animation_length_1 + BLANK_FRAMES)

# 2つ目のアニメーションの長さを取得
animation_length_2 = int(max(fcurve.keyframe_points[-1].co[0] for fcurve in action2.fcurves)) - (animation_length_1 + BLANK_FRAMES)

# シーンの終了フレームを更新
bpy.context.scene.frame_end = animation_length_1 + BLANK_FRAMES + animation_length_2

# アーマチュアオブジェクトを取得
armature = next((obj for obj in bpy.data.objects if obj.type == 'ARMATURE'), None)

if armature:
    # 既存のアニメーションデータを削除
    if armature.animation_data:
        armature.animation_data_clear()
    
    # 新しいアニメーションデータを作成
    armature.animation_data_create()
    
    # 新しいNLAトラックを作成
    track = armature.animation_data.nla_tracks.new()
    
    # 最初のアニメーションをNLAストリップとして追加
    strip1 = track.strips.new(action1.name, 0, action1)
    
    # 2つ目のアニメーションをNLAストリップとして追加(空白時間を考慮)
    strip2 = track.strips.new(action2.name, animation_length_1 + BLANK_FRAMES, action2)

print(f"アニメーションのインポートと結合が完了しました。2つのアニメーションの間に{BLANK_FRAMES}フレームの空白時間を追加しました。")

不要なフレームの削除する。(今回の場合は、↑のスクリプトで追加したフレームを削除する。)

import bpy

# 削除する開始フレームと終了フレーム
START_FRAME = 66
END_FRAME = 67

def delete_frames_and_shift(action, start_frame, end_frame):
    frames_to_delete = end_frame - start_frame + 1
    
    for fcurve in action.fcurves:
        # 削除するキーフレームを特定
        keyframes_to_remove = [kf for kf in fcurve.keyframe_points if start_frame <= kf.co[0] <= end_frame]
        
        # キーフレームを削除
        for kf in reversed(keyframes_to_remove):
            fcurve.keyframe_points.remove(kf)
        
        # 残りのキーフレームをシフト
        for kf in fcurve.keyframe_points:
            if kf.co[0] > end_frame:
                kf.co[0] -= frames_to_delete
        
        # FCurveを更新
        fcurve.update()

# アーマチュアオブジェクトを取得
armature = next((obj for obj in bpy.data.objects if obj.type == 'ARMATURE'), None)

if armature and armature.animation_data and armature.animation_data.nla_tracks:
    for track in armature.animation_data.nla_tracks:
        for strip in track.strips:
            # ストリップのアクションを取得
            action = strip.action
            if action:
                # フレームを削除してシフト
                delete_frames_and_shift(action, START_FRAME, END_FRAME)
                
                # ストリップの長さを調整
                frames_to_delete = END_FRAME - START_FRAME + 1
                if strip.action_frame_start >= END_FRAME:
                    strip.action_frame_start -= frames_to_delete
                if strip.action_frame_end > START_FRAME:
                    strip.action_frame_end -= frames_to_delete
    
    # シーンの終了フレームを更新
    bpy.context.scene.frame_end -= (END_FRAME - START_FRAME + 1)
    
    print(f"フレーム {START_FRAME} から {END_FRAME} までを完全に削除し、後続のフレームをシフトしました。")
else:
    print("アニメーションが見つかりません。")

# タイムラインを更新
bpy.context.scene.frame_set(bpy.context.scene.frame_current)

# アニメーションデータを更新
bpy.context.view_layer.update()

これまでのスクリプトだとまだモーションが統合されていないので、以下のスクリプトで統合する。

import bpy

def set_nla_editor_context():
    for area in bpy.context.screen.areas:
        if area.type == 'NLA_EDITOR':
            override = bpy.context.copy()
            override['area'] = area
            override['region'] = area.regions[-1]
            return override
    return None

def join_nla_strips():
    armature = next((obj for obj in bpy.data.objects if obj.type == 'ARMATURE'), None)
    if armature and armature.animation_data and armature.animation_data.nla_tracks:
        track = armature.animation_data.nla_tracks[0]
        strips = track.strips
        if len(strips) > 1:
            override = set_nla_editor_context()
            if override:
                bpy.ops.nla.select_all(override, action='SELECT')
                bpy.ops.nla.join_strips(override)
            else:
                print("NLAエディターが見つかりません。手動でNLAエディターを開いてから再試行してください。")

def bake_action():
    armature = next((obj for obj in bpy.data.objects if obj.type == 'ARMATURE'), None)
    if armature:
        bpy.context.view_layer.objects.active = armature
        bpy.ops.nla.bake(frame_start=bpy.context.scene.frame_start, frame_end=bpy.context.scene.frame_end, step=1, only_selected=False, visual_keying=True, clear_constraints=False, clear_parents=False, use_current_action=False, bake_types={'POSE'})

def apply_action_to_armature():
    armature = next((obj for obj in bpy.data.objects if obj.type == 'ARMATURE'), None)
    if armature:
        new_action = bpy.data.actions[-1]  # 最後に作成されたアクションを取得
        if armature.animation_data is None:
            armature.animation_data_create()
        armature.animation_data.action = new_action

# NLAストリップを結合
join_nla_strips()

# アクションをベイク
bake_action()

# 新しいアクションをアーマチュアに適用
apply_action_to_armature()

print("アニメーションの結合とベイクが完了しました。VRMエクスポート機能を使用してHumanoidアニメーションとして出力できます。")

以下からVRMAをエクスポートする。

結果

以下のように統合された状態でVRMAがエクスポートされる。

おまけ

↑の動画は以下のスクリプトで出力しています。

import bpy
import math
import os
from datetime import datetime

# ユーザーが調整可能なパラメータ
distance_from_model = 3.0       # モデルからの距離
camera_height = 2.5             # カメラの高さ
camera_angle = 60.0             # カメラの角度(度単位)
output_path = r"C:\Users\XXXXX\XXXXX"

# 回転の中心座標
pivot_x = 0.0
pivot_y = 0.0
pivot_z = 0.0

# 回転の開始角度(度単位)
start_angle = -45.0

# 解像度の指定
resolution_x = 1080             # 横解像度
resolution_y = 1920             # 縦解像度

# フレームレートと回転角度の指定
frame_rate = 30                 # フレームレート(fps)
rotation_degrees = 0          # カメラの回転角度(度単位)

# 既存のカメラを削除
for obj in bpy.data.objects:
    if obj.type == 'CAMERA':
        bpy.data.objects.remove(obj, do_unlink=True)

# 新しいカメラを追加
bpy.ops.object.camera_add()
camera = bpy.context.object
camera.name = "RotatingCamera"

# カメラの角度をラジアンに変換
camera_angle_rad = math.radians(camera_angle)

# カメラの初期位置を設定
camera.location = (distance_from_model, 0, camera_height)
camera.rotation_euler = (camera_angle_rad, 0, math.pi / 2)

# 回転の中心となる空オブジェクトを指定した座標に追加
bpy.ops.object.empty_add(type='PLAIN_AXES', location=(pivot_x, pivot_y, pivot_z))
pivot = bpy.context.object
pivot.name = "CameraPivot"

# カメラをピボットにペアレント設定
camera.parent = pivot

# シーンの設定
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = 220  # アニメーションのフレーム数(例として10秒間のアニメーション)

# フレームレートを設定
scene.render.fps = frame_rate

# カメラを回転させるためのキーを設定
start_angle_rad = math.radians(start_angle)
pivot.rotation_euler = (0, 0, start_angle_rad)
pivot.keyframe_insert(data_path="rotation_euler", frame=scene.frame_start)

# 回転角度をラジアンに変換
end_angle_rad = math.radians(start_angle + rotation_degrees)
pivot.rotation_euler = (0, 0, end_angle_rad)
pivot.keyframe_insert(data_path="rotation_euler", frame=scene.frame_end)

# 補間を線形に設定
for fcurve in pivot.animation_data.action.fcurves:
    for keyframe in fcurve.keyframe_points:
        keyframe.interpolation = 'LINEAR'

# タイムスタンプを生成
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")

# レンダリング出力設定
scene.render.image_settings.file_format = 'FFMPEG'
scene.render.ffmpeg.format = 'MPEG4'
scene.render.filepath = os.path.join(output_path, f"animation_{timestamp}.mp4")

# 解像度を設定
scene.render.resolution_x = resolution_x
scene.render.resolution_y = resolution_y
scene.render.resolution_percentage = 100  # 解像度の割合(100%で指定した解像度)

# 出力ディレクトリが存在しない場合は作成
if not os.path.exists(output_path):
    os.makedirs(output_path)

# カメラをアクティブカメラに設定
scene.camera = camera

# アニメーションをレンダリング
bpy.ops.render.render(animation=True)

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