![見出し画像](https://assets.st-note.com/production/uploads/images/157951178/rectangle_large_type_2_8f295b83586691891a4bf4e7df197979.png?width=1200)
Blenderで複数VRMAの統合・修正・出力
Blenderのスクリプトを使って、2つのVRMAを加工して、1つのVRMAと出力できたので、そのスクリプトを残しておく。
みはるが投稿
— kongo jun (@jun_kongo) October 14, 2024
2つのVRMAを1つに統合 pic.twitter.com/1cnmkUOgCi
環境構築
以下の記事などを参考に、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をエクスポートする。
![](https://assets.st-note.com/img/1728907984-j6JPO4XwTasMEphlVSNkqBtZ.png)
結果
以下のように統合された状態でVRMAがエクスポートされる。
みはるが投稿
— kongo jun (@jun_kongo) October 14, 2024
2つのVRMAを1つに統合 pic.twitter.com/1cnmkUOgCi
おまけ
↑の動画は以下のスクリプトで出力しています。
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)