本日はBlender枠です。
昨日に引き続きBlenderのGIFエクスポーターの改良を行います。
今回はGIF画像生成において出力される一時データを削除して操作性を向上させて生きます。
〇現在のコード
#アドオンの定義 bl_info = { "name": "GIFMaker", "blender": (3, 5, 0), "category": "Object", } import bpy import os import re import subprocess # 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 LoopRenderOperator(bpy.types.Operator): bl_idname = "object.loop_render" bl_label = "Loop Render" def execute(self, context): 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 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') 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.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 =True) 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()
〇シーケンスの一時画像の削除
現状のコードではレンダリング結果のシーケンス画像の出力パスとGIF画像を出力するパスは任意に設定できますが、問題点としてGIF画像を作る際にディレクトリ内のすべての画像を使用してGIF画像を作ります。
つまり、ディレクトリ内にGIF画像を作成するために不要な画像が入っていた場合その画像もGIF画像に組み込まれてしまうことになります。
このため、一回一回フォルダ内のファイルを削除することが推奨されていますが、非常に面倒くさいです。
シーケンス画像をGIF画像生成後に削除してしまいます。
まずはレンダリング結果のシーケンス画像以外のファイルが混入しないようにUI上で指定したパスの子階層を作成してシーケンス画像を保存するようにします。
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}\\temp' 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'}
上記処理ではデフォルトでのBlenderの保存パスであるC:\tmpにさらにディレクトリを作成してそこを保存パスとしています。
この場合最終成果物であるgif画像は一つ上のC:\tmp
に保存されます。
あとはgif画像作成後にC:\tmp\tmp
のディレクトリごと消してしまえば成果物であるgif画像のみ残されます。
Pythonで特定のディレクトリを削除するには次の処理を実行します。
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'}
なおこの処理によって消されるディレクトリは復元できないのでパスは注意してください。
この処理を加えることによってシーケンス画像が削除され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 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.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()