夜風のMixedReality

xRと出会って変わった人生と出会った技術を書き残すためのGeekなHoloRangerの居場所

BlenderでGIF書き出しプラグインを作る その② GIF画像の作成

本日はBlender及びPython枠です。

昨日Blenderレンダリングのシーケンス画像を作成する処理を記述しました。

redhologerbera.hatenablog.com

本日はこちらをもとにGIF画像を作成する処理を組み合わせていきます。

〇GIF画像の生成

GIF画像の生成自体は昨日の記事を参考にしてください

redhologerbera.hatenablog.com

GIF画像の生成自体は仮想環境を立てて実行していたので今回も同様に仮想環境ごと実行させていきます。

Blenderで仮想環境ごと外部Pythonプロジェクトを実行する手順も以前に記事にしています。

今回はこちらを応用します。

redhologerbera.hatenablog.com

こちらの記事内ではInput,Outputのパスがプロジェクト内に存在しました。

InputのパスはBlenderレンダリング出力パスと一致させる必要があります。

そのためまずはBlender側から変数としてパスを扱えるようにしてかつパスを書き換える実装を行います。

まずは一時レンダリングのシーケンス画像の出力場所のパスを明示的に変更できるようにします。

今回の場合は流用した最初のひな型でテキストフィールドを用意しているのでこちらを使用します。

まずはregister関数で登録する際に表示するテキストフィールドの説明をTemp Pathとします。

def register():
   ・・・
    bpy.types.Scene.my_text_input = bpy.props.StringProperty(name="Temp Path:")
 ・・・

これによって表示がTemp Pathに変わります。

デフォルトのパスが欲しいので第二引数としてdefaultを持たせます。

    bpy.types.Scene.my_text_input = bpy.props.StringProperty(name="Temp Path:",default="tmp/")

これによって初期のパスが入力されるようになりました。

 次にこのパスと一時的なレンダリングシーケンス画像の保存パスを一致させます。

 テキストフィールドの値は今回の実装ではbpy.context.scene.my_text_inputを使用すればよいです。

つまりレンダリング時のパスの指定を次のように変更します。

class LoopRenderOperator(bpy.types.Operator):
    bl_idname = "object.loop_render"
    bl_label = "Loop Render"

    start_frame: bpy.props.IntProperty(name="Start Frame", default=1)
    end_frame: bpy.props.IntProperty(name="End Frame", default=20)
    step: bpy.props.IntProperty(name="Step", default=1)

    def execute(self, context):
        for frame in range(self.start_frame, self.end_frame + 1, self.step):
            bpy.context.scene.frame_set(frame)
            bpy.context.scene.render.filepath = f"{bpy.context.scene.my_text_input}/render_{frame}" #変更            bpy.ops.render.render(write_still=True) 
        return {'FINISHED'}

 これによってテキストフィールドで指定したパスがレンダリングのシーケンス画像のパスになります。

ここまでが変数としてパスを扱えるようにする部分になります。

ここから変数として指定したパスを使用して実際にGIFを作る外部スクリプトのパスを書き換えます。

こちらも過去に行っているものを使用します。

redhologerbera.hatenablog.com

上記記事ではBatファイルを書き換えていますが、基本的にスクリプトもテキストファイルのため拡張子を変えてあげるだけで流用することができます。

まずは次のようにpythonスクリプトを読みこみます。

class GIFMakerInit():
    gif_maker_file_path = r'C:(任意のパス)GIFCreator\gif_creator.py'
    with open(gif_maker_file_path, 'r') as f:
        content = f.read()
    print("読み込んだテキスト: ", content)

実行してコンソールを見るとスクリプトの読み込みができていることが確認できます。

〇パスの書き換え

パスの書き換えに対応させるためにシーケンス画像からGIF作成を行うスクリプトで埋め込みで記述していたパスを外部に出します。

input_path = r'C:\(パス)\Gifinput'
gif_output_path = r'C:\Users\(パス)\output.gif'
create_gif(input_path, gif_output_path)
check_gif()

つまりinput_path = r'・・・'のパターンを見つけ内部の・・・を書き換えるコードを実装することになります。

これを行うためには次のように条件を与えてマッチする文字列を取得します。

    pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    matches = re.findall(pattern, content)

for文で見てみると次のような結果を得ることができます。

    for match in matches:
        print(f'match[0]"{match[0]}')
        print(f'match[1]"{match[1]}')
        print(f'match[2]"{match[2]}')

パスは一例です

つまりmatch[1]を差し替えればよいということになります。

これを前回行った出力先に対応させると次のようになります。

    for match in matches:
        # match[0]は"input_path = r\"", match[1]は"..."(ダブルクォート内のパス), match[2]は"\""
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}{bpy.context.scene.my_text_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)

最後に元のファイルの書き換えを行うと次のようになります。

def gif_maker_init():
    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()    

    pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    matches = re.findall(pattern, content)
    
    new_content = content
    for match in matches:
        # match[0]は"input_path = r\"", match[1]は"..."(ダブルクォート内のパス), match[2]は"\""
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}{bpy.context.scene.my_text_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    with open(gif_maker_file_path, 'w') as f:
        f.write(new_content)

以上でシーケンス画像を読み込む場所をBlenderレンダリング結果の出力パスにできました。

〇GIF作成の実行

続いてGIF作成スクリプトを実行していきます。

次のように実装しました。

def gif_maker_excute(script_path):
    python_path = r"C:\\anaconda3\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():
    gif_maker_file_path = r'C:\Users\seiri\Documents\PythonStudy\GIFCreator\gif_creator.py'
    with open(gif_maker_file_path, 'r') as f:
    ・・・
    gif_maker_excute(gif_maker_file_path)#追加

ここまでのコードは次のようになります。

#アドオンの定義
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, "my_text_input")
        layout.operator("object.loop_render")
        # Add a text field to the panel



        
class LoopRenderOperator(bpy.types.Operator):
    bl_idname = "object.loop_render"
    bl_label = "Loop Render"

    start_frame: bpy.props.IntProperty(name="Start Frame", default=1)
    end_frame: bpy.props.IntProperty(name="End Frame", default=20)
    step: bpy.props.IntProperty(name="Step", default=1)

    def execute(self, context):
        for frame in range(self.start_frame, self.end_frame + 1, self.step):
            bpy.context.scene.frame_set(frame)
            bpy.context.scene.render.filepath = f"{bpy.context.scene.my_text_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():
    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()    

    pattern = r'(input_path\s*=\s*r\s*[\'\"])(.*?)([\'\"])'
    matches = re.findall(pattern, content)
    
    new_content = content
    for match in matches:
        # match[0]は"input_path = r\"", match[1]は"..."(ダブルクォート内のパス), match[2]は"\""
        old_string = f'{match[0]}{match[1]}{match[2]}'
        new_string = f'{match[0]}C:\{bpy.context.scene.my_text_input}{match[2]}'
        new_content = new_content.replace(old_string, new_string)
    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.my_text_input = bpy.props.StringProperty(name="Temp Path:",default="tmp")

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()

これを実行すると20フレーム分のレンダリングが行われGIF画像が作成されます。

以上でBlenderでGIF画像の作成ができました。

次回UIの改善等を行っていきます。