前回まででBlenderでGIFのレンダリングプラグインを作成しました。
もともと作成の動機としてはGIF画像を作成する際によいソフトウェアが見つからなかったため自作していました。今回は仕上げとして動画編集機能にGIFエクスポートの機能を実装して、動画編集機能としてのGIFサポートを実装します。
〇Blenderの動画編集機能
Blenderを使用する人の多くは3DCGの作成を挙げると思います。筆者も主にこの用途で使用していますが、Blender自体には高度な動画編集機能を備えています。

動画ファイルおよび今回は使用しませんが音源ファイルをChannnelのウィンドウにドラッグアンドドロップすると動画がエディタに読み込まれます。

チャンネルに登録されているファイルから任意のキーフレームでKキーを押すことでそのフレームでファイルをカットすることができます。

通常は上部メニューレンダーからアニメーションレンダリングを選択することでレンダリングが始まり、出力プロパティに指定されている出力パス、フォーマットに従ってレンダリングがされます。


〇プラグインUIの場所
基本的な考え方は過去のものと同じで、シーケンス画像として出力した画像とパスを使用してGIFを作成するスクリプトを実行します。
つまるところGIFを作成するコードは変更せずにBlenderのPythonスクリプトを変更してビデオエディタでも使用できるようにしていきます。
具体的には3Dビューポート上に新しくタブを作る方向性や上部のレンダーコマンドでレンダリングを実行させるUIを実装するような流れになりそうです。


〇動画編集エディタでのUI登録
GIFExporterPanlクラスを複製し新しくGIFExporterPanelVideoEditorクラスを作成します。
またbl_space_typeをSEQUENCE_EDITORに変更します。
class GIFExporterPanelVideoEditor(bpy.types.Panel):
bl_label = "Video Editor Exporter"
bl_idname = "SEQUENCE_PT_video_editor_exporter"
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
bl_category = 'GIFExporter'
def draw(self, context):
layout = self.layout
layout.prop(context.scene, "gif_input")
layout.prop(context.scene, "gif_output")
layout.prop(context.scene, "loop_start_frame")
layout.prop(context.scene, "loop_end_frame")
layout.prop(context.scene, "loop_step_frame")
layout.label(text="GIF Export Settings")
layout.prop(context.scene, "use_duration")
if context.scene.use_duration:
layout.prop(context.scene, "gif_duration")
# Execute button for video editing
layout.label(text="Execute Video Editing")
layout.operator("object.video_editor_render")
またオペレータークラスも作成します。
class VideoEditorRenderOperator(bpy.types.Operator):
bl_idname = "object.video_editor_render"
bl_label = "Video Editor Render"
def excute(self,context):
self.report({'INFO'}, "Hello, World!")
return {'FINISHED'}
最後にこの二つのクラスをBlenderに登録していきます。
def register():
bpy.utils.register_class(GIFExporterOperator)
bpy.utils.register_class(GIFExporterPanel)
bpy.utils.register_class(LoopRenderOperator)
bpy.utils.register_class(VideoEditorRenderOperator)#追加
bpy.utils.register_class(GIFExporterPanelVideoEditor)#追加
bpy.types.Scene.gif_input = bpy.props.StringProperty(name="Temp Path:",default="tmp")
・・・
これを実行することでVideoEditorにタブが現れます。

次にレンダリングの処理をVideoEditorに合わせます。
具体的には次のようなコードになります。
class VideoEditorRenderOperator(bpy.types.Operator):
bl_idname = "object.video_editor_render"
bl_label = "Video Editor Render"
def execute(self, context):
bpy.context.scene.gif_input = f'{bpy.context.scene.gif_input}\\tmp'
print(bpy.context.scene.gif_input)
# Switch to the Video Sequence Editor
bpy.context.area.type = 'SEQUENCE_EDITOR'
# Set the frame range for rendering
bpy.context.scene.frame_start = bpy.context.scene.loop_start_frame
bpy.context.scene.frame_end = bpy.context.scene.loop_end_frame
for frame in range(bpy.context.scene.loop_start_frame, bpy.context.scene.loop_end_frame + 1, bpy.context.scene.loop_step_frame):
# Set the current frame
bpy.context.scene.frame_set(frame)
# Update the Scene render file path
bpy.context.scene.render.filepath = f"{bpy.context.scene.gif_input}/render_{frame}"
# Perform OpenGL rendering
bpy.ops.render.opengl(animation=False, sequencer=True, write_still=True)
# Switch back to the 3D Viewport
bpy.context.area.type = 'VIEW_3D'
gif_maker_init()
return {'FINISHED'}
これは3Dのレンダリングではbpy.ops.render.renderが使用できますが、動画編集エディタでは使用できないためOpenGLを使用したスクリプトでのレンダリングを使用します。
これによって動画編集にも対応したGIFエクスポーターができました。
3Dにも2Dにもどちらにも対応しています。
本日は以上です。
なお、一般的なGIF画像作成ツールの場合圧縮なども行われており、データ量を削減したりもしているようなのでその機能も後々実装していきます。
〇コード全文
#アドオンの定義
bl_info = {
"name": "GIFMaker",
"blender": (3, 5, 0),
"category": "Object",
}
import bpy
import os
import re
import subprocess
import shutil
# Define a new operator (action or function)
class GIFExporterOperator(bpy.types.Operator):
bl_idname = "object.gif_maker"
bl_label = "Hello, World!"
def execute(self, context):
self.report({'INFO'}, "Hello, World!")
return {'FINISHED'}
# Define a new UI panel
class GIFExporterPanel(bpy.types.Panel):
bl_label = "GIF Exporter"
bl_idname = "OBJECT_PT_hello_world"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'GIFExporter'
def draw(self, context):
layout = self.layout
layout.prop(context.scene, "gif_input")
layout.prop(context.scene, "gif_output")
layout.prop(context.scene, "loop_start_frame")
layout.prop(context.scene, "loop_end_frame")
layout.prop(context.scene, "loop_step_frame")
layout.label(text="GIF Export Settings")
layout.prop(context.scene, "use_duration")
if context.scene.use_duration:
layout.prop(context.scene, "gif_duration")
layout.label(text="Excute")
layout.operator("object.loop_render")
class VideoEditorRenderOperator(bpy.types.Operator):
bl_idname = "object.video_editor_render"
bl_label = "Video Editor Render"
def execute(self, context):
self.report({'INFO'}, "Hello, World!")
return {'FINISHED'}
class GIFExporterPanelVideoEditor(bpy.types.Panel):
bl_label = "Video Editor Exporter"
bl_idname = "SEQUENCE_PT_video_editor_exporter"
bl_space_type = 'SEQUENCE_EDITOR'
bl_region_type = 'UI'
bl_category = 'GIFExporter'
def draw(self, context):
layout = self.layout
layout.prop(context.scene, "gif_input")
layout.prop(context.scene, "gif_output")
layout.prop(context.scene, "loop_start_frame")
layout.prop(context.scene, "loop_end_frame")
layout.prop(context.scene, "loop_step_frame")
layout.label(text="GIF Export Settings")
layout.prop(context.scene, "use_duration")
if context.scene.use_duration:
layout.prop(context.scene, "gif_duration")
# Execute button for video editing
layout.label(text="Execute Video Editing")
layout.operator("object.video_editor_render")
class VideoEditorRenderOperator(bpy.types.Operator):
bl_idname = "object.video_editor_render"
bl_label = "Video Editor Render"
def execute(self, context):
bpy.context.scene.gif_input = f'{bpy.context.scene.gif_input}\\tmp'
print(bpy.context.scene.gif_input)
# Switch to the Video Sequence Editor
bpy.context.area.type = 'SEQUENCE_EDITOR'
# Set the frame range for rendering
bpy.context.scene.frame_start = bpy.context.scene.loop_start_frame
bpy.context.scene.frame_end = bpy.context.scene.loop_end_frame
# Set the render output format to a single file format (e.g., PNG)
bpy.context.scene.render.image_settings.file_format = 'PNG'
for frame in range(bpy.context.scene.loop_start_frame, bpy.context.scene.loop_end_frame + 1, bpy.context.scene.loop_step_frame):
# Set the current frame
bpy.context.scene.frame_set(frame)
# Update the Scene render file path
bpy.context.scene.render.filepath = f"{bpy.context.scene.gif_input}/render_{frame}"
# Perform OpenGL rendering
bpy.ops.render.opengl(animation=False, sequencer=True, write_still=True)
# Switch back to the 3D Viewport
bpy.context.area.type = 'VIEW_3D'
gif_maker_init()
return {'FINISHED'}
class LoopRenderOperator(bpy.types.Operator):
bl_idname = "object.loop_render"
bl_label = "Loop Render"
def execute(self, context):
bpy.context.scene.gif_input = f'{bpy.context.scene.gif_input}\\tmp'
print(bpy.context.scene.gif_input)
for frame in range(bpy.context.scene.loop_start_frame, bpy.context.scene.loop_end_frame + 1, bpy.context.scene.loop_step_frame):
bpy.context.scene.frame_set(frame)
bpy.context.scene.render.filepath = f"{bpy.context.scene.gif_input}/render_{frame}"
bpy.ops.render.render(write_still=True)
gif_maker_init()
return {'FINISHED'}
def cleanup_gif_maker():
dir_path = f'C:\\{bpy.context.scene.gif_input}'
if os.path.exists(dir_path):
shutil.rmtree(dir_path)
else:
print("The directory does not exist")
return {'FINISHED'}
def gif_maker_excute(script_path):
python_path = r"C:\Users\seiri\anaconda3\envs\gif-creator\python.exe"
if not os.path.exists(script_path):
print(f"スクリプトが見つかりません: {script_path}")
else:
# subprocessを使用してスクリプトを実行します
result = subprocess.run([python_path, script_path], capture_output=True, text=True,encoding='utf-8',errors='replace')
cleanup_gif_maker()
return {'FINISHED'}
def gif_maker_init():
if not bpy.context.scene.use_duration:
# 現在のシーンを取得
current_scene = bpy.context.scene
# 現在のシーンのfpsを取得
fps = current_scene.render.fps
bpy.context.scene.gif_duration = int(bpy.context.scene.loop_step_frame * 1000 / fps)
gif_maker_file_path = r'C:\Users\seiri\Documents\PythonStudy\GIFCreator\gif_creator.py'
with open(gif_maker_file_path, 'r') as f:
content = f.read()
input_pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
output_pattern = r'(gif_output_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
duration_pattern = r'(duration_num_str\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
input_matches = re.findall(input_pattern, content)
output_matches = re.findall(output_pattern, content)
duration_matches = re.findall(duration_pattern, content)
print(len(duration_matches))
new_content = content
for match in input_matches:
old_string = f'{match[0]}{match[1]}{match[2]}'
new_string = f'{match[0]}C:\{bpy.context.scene.gif_input}{match[2]}'
new_content = new_content.replace(old_string, new_string)
for match in output_matches:
old_string = f'{match[0]}{match[1]}{match[2]}'
new_string = f'{match[0]}C:\{bpy.context.scene.gif_output}\output.gif{match[2]}'
new_content = new_content.replace(old_string, new_string)
for match in duration_matches:
old_string = f'{match[0]}{match[1]}'
new_string = f'{match[0]}{bpy.context.scene.gif_duration}'
new_content = new_content.replace(old_string, new_string)
print(new_content)
with open(gif_maker_file_path, 'w') as f:
f.write(new_content)
gif_maker_excute(gif_maker_file_path)
def register():
bpy.utils.register_class(GIFExporterOperator)
bpy.utils.register_class(GIFExporterPanel)
bpy.utils.register_class(LoopRenderOperator)
bpy.utils.register_class(VideoEditorRenderOperator)
bpy.utils.register_class(GIFExporterPanelVideoEditor)
bpy.types.Scene.gif_input = bpy.props.StringProperty(name="Temp Path:",default="tmp")
bpy.types.Scene.gif_output = bpy.props.StringProperty(name="GIFOutput Path:",default="tmp")
bpy.types.Scene.loop_start_frame = bpy.props.IntProperty(name="Start Frame", default=1) # Add this line
bpy.types.Scene.loop_end_frame = bpy.props.IntProperty(name="End Frame", default =20)
bpy.types.Scene.loop_step_frame = bpy.props.IntProperty(name="Step",default =1)
bpy.types.Scene.use_duration = bpy.props.BoolProperty(name="Use_Duration",default =False)
bpy.types.Scene.gif_duration = bpy.props.IntProperty(name="GIF Duration(/mSec)" ,default = 1)
def unregister():
bpy.utils.unregister_class(GIFExporterOperator)
bpy.utils.unregister_class(GIFExporterPanel)
bpy.utils/unregister_class(LoopRenderOperator)
del bpy.types.Scene.my_text_input
if __name__ == "__main__":
register()