Blender CUI ベイク（Physics）とその進捗表示

:::note info
2023/04/12
タイトル変更が正しくなかったのを修正。
Cyclesレンダリング時のデバイスを指定出来る様にしました。
2023/04/09
Physicsの終了時間が分かる様にレンダリング開始時間をタイトルに追加する様にしました。
進捗率の計算がおかしかったので修正しました（はず）
2023/04/08
ETCの算出式がおかしかったので修正しました。
:::



# 初めに
BlenderをCUI上でシミュレーションベイクをするのがほぼ日課なのですが、
```Python:Blender Python
    bpy.ops.ptcache.bake_all(bake=True)
```
などでBlender Pythonを実行するとGUIとは違い進捗やパフォーマンスが表示されません。
![0307.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/802449/f5b71646-176c-0e3f-3978-29317b074e76.jpeg)

この件で改善策が無いか情報を探していたのですが特に見つからず。
海外フォーラムで質問してみた所ヒントを貰ってあっさり解決したので備忘録として残しておきます。

↓動作イメージ
![0310.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/802449/01d68112-0c2b-0f97-4ff0-340de83314d1.jpeg)


# CUIでPythonスクリプトを指定して起動する（Windows)
--python オプションを付けて起動するだけです。以下例
```bat: run.bat（Windows）
chcp 65001
@echo off

set FILENAME=ここにファイル名
set FILEPATH=C:\tmp\
set BLENDER="C:\Program Files\Blender Foundation\Blender 3.5\blender.exe"
set BPY="C:\Users\niuin\OneDrive\blender\scripts\ptcachebake.py"

%BLENDER% -b "%FILEPATH%%FILENAME%.blend" --python %BPY%
pause
```
__chcp 65001__: 文字コード指定。日本語が含まれると文字化けするので必須です。　
__@&#xFFF0;echo off__:  冗長なコマンド入力の復唱を省略します。
__FILENAME__: .blendファイル名を指定して下さい（拡張子不要）
__FILEPATH__: .blendファイルまでのフォルダパスを指定して下さい。
__BLENDER__: blender.exeへのファイルパスを指定して下さい。
__BPY__: Pythonスクリプトへのファイルパスを指定して下さい。
__-b__: Blenderのバックグラウンド起動オプションです。


<details><summary><ins><b>物理ベイク→(FlipFluids)→動画レンダリングもしたい時の例</ins></b>
</summary>

時刻取得してどのプロセスか判断出来る様にPIDも取得してタイトルに追加します。
```bat: run.bat（Windows）
chcp 65001
@echo off


rem 処理スイッチ true/false
set physics=true
set fluid=false
set rendering=true
set lowrender=true
set ren_device=CUDA+CPU
rem CPU, CUDA, OPTIX, HIP, ONEAPI, METAL, Append "+CPU"

rem ファイルパス設定
set FILENAME=ここにファイル名
set FILEPATH=C:\tmp\
set BLENDER="C:\blender\blender-3.5.1-candidate+v35.1bd46a46d15a-windows.amd64-release\blender.exe"
set BPY="C:\Users\niuin\OneDrive\blender\scripts\ptcachebake.py"
set OUTPUT="C:\Users\niuin\OneDrive\blender\output"
set FLIPFLUIDS_SCRIPT="C:\Users\niuin\AppData\Roaming\Blender Foundation\Blender\3.5\scripts\addons\flip_fluids_addon\resources\command_line_scripts\run_simulation.py"


rem 設定ここまで
rem ---------------------------------

rem Poweshell経由で現在時刻を取得しSTART_TIMEに格納
set ps_command=`powershell "(date).ToString('yyyy-MM-dd HH:mm:ss')"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set START_TIME=%%A

rem ファイルネーム用時刻をFILE_TIMEに格納
set ps_command=`powershell "(date).ToString('yyyyMMdd_HHmm')"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set FILE_TIME=%%A

rem PID取得してタイトル変更
title [normal] %FILENAME%   START: %START_TIME%
for /F "tokens=2" %%i in ('tasklist /FI "WINDOWTITLE eq [normal] %FILENAME%   START: %START_TIME%" /NH') do set MyPid=%%i
title [normal] %FILENAME%   START: %START_TIME%   PID:%MyPid%

rem Pythonスクリプトを指定してBlenderをバックグラウンド起動
IF %physics%==true (
	echo "------------- Load Script -------------"
	%BLENDER% -b "%FILEPATH%%FILENAME%.blend" --python %BPY%
    echo "------------- End Script-------------"
	) 

IF %fluid%==true (
	echo "------------- Fluid Bake Start -------------"
	%BLENDER% -b "%FILEPATH%%FILENAME%.blend" --python %FLIPFLUIDS_SCRIPT%
	echo "------------- Fluid Bake End -------------"
	)

rem "レンダリング開始時間を格納"
set ps_command=`powershell "(date).ToString('yyyy-MM-dd HH:mm:ss')"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set REN_START=%%A


IF %lowrender%==true (
	title [low] %FILENAME%   START: %START_TIME%   PID:%MyPid%   REN_START^(%ren_device%^): %REN_START%
	wmic process where processid=%MyPid% call setpriority "idle"
	) else (
	title [normal] %FILENAME%   START: %START_TIME%   PID:%MyPid%   REN_START^(%ren_device%^): %REN_START%
	)

echo %rendering%

IF %rendering%==true (
	echo "............. Rendering Start ............."
	%BLENDER% -b "%FILEPATH%%FILENAME%.blend" -o %OUTPUT%\%FILENAME%_%FILE_TIME% -a -- --cycles-device %ren_device%
	echo "............. Rendering End ............."
	)

rem "終了時刻をEND_TIMEに格納"
set ps_command=`powershell "(date).ToString('yyyy-MM-dd HH:mm:ss')"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set END_TIME=%%A

rem end_time - start_timeの差をDIFF_TIMEに格納
set ps_command=`powershell "([datetime]\"%END_TIME%\"-[datetime]\"%START_TIME%\").Hours"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set DIFF_TIME_H=%%A
set ps_command=`powershell "([datetime]\"%END_TIME%\"-[datetime]\"%START_TIME%\").Minutes"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set DIFF_TIME_M=%%A
set ps_command=`powershell "([datetime]\"%END_TIME%\"-[datetime]\"%START_TIME%\").Seconds"`
for /F "usebackq delims=" %%A IN (%ps_command%) DO set DIFF_TIME_S=%%A

echo "%FILENAME% Processing done!"

echo 開始時間：%START_TIME%
echo 終了時間：%END_TIME%
echo 経過時間：%DIFF_TIME_H%:%DIFF_TIME_M%:%DIFF_TIME_S%

pause
```
__physics__: trueで物理シミュのベイクをするスクリプトを呼びます。falseで何もしません。
__fluid__: trueでFlipfluidsをベイクをするスクリプトを呼びます。falseで何もしません。
__rendering__: trueでアニメーションレンダリングをします。falseで何もしません。
__lowrender__: trueでレンダリング時OSの処理優先度を低に変更します。他の作業に影響与えたくない時にオススメです。
__OUTPUT__: レンダリングの成果物を出力するフォルダパスを指定して下さい。
__ren_device__: Cyclesレンダリング時、使用するデバイスを指定します。
指定デバイス: CPU, CUDA, OPTIX, HIP, ONEAPI, METAL, Append "+CPU"
参考: https://docs.blender.org/manual/en/latest/advanced/command_line/render.html#cycles
</details>


# PythonスクリプトでCUIベイク時に進捗表示させる

```python: ptcachebake.py
import bpy, math
from datetime import datetime, timedelta

print("----- Script Run")

# ベイク済みキャッシュを全開放します
bpy.ops.ptcache.free_bake_all()
print("--- physics bake free!")

# 初期化
g_previous_time: datetime = ""
g_total_time: float = 0.0
# フレームを開始フレームに移動しておく
bpy.context.scene.frame_set(bpy.context.scene.frame_start)

def my_handler(scene):

    current_time = datetime.now()
    global g_previous_time
    global g_total_time
    previous_time = g_previous_time
    g_previous_time = current_time

    if previous_time and 0 != (scene.frame_current - scene.frame_start):
        difference_time = current_time - previous_time
        g_total_time += difference_time.total_seconds()
        etctime = ((g_total_time / (scene.frame_current - scene.frame_start)) * (scene.frame_end - scene.frame_current))
        print(
                str("{:.1f}".format(difference_time.total_seconds())) + " sec " + 
                str("(avg {:.1f}".format((g_total_time / (scene.frame_current - scene.frame_start)))) + "s) / [" + 
                str(math.floor((g_total_time)/3600)) + "h " + 
                str(math.floor(((g_total_time)/60)%60)) + "m " + 
                str("{:.0f}".format(g_total_time%60)) + "s] " + 
                str("-----> {:.1f}".format(((scene.frame_current - (scene.frame_start)) / (scene.frame_end - (scene.frame_start - 1))*100))) + "% " +
                "-----> ETC:[" +
                str(math.floor((etctime)/3600)) + "h " + 
                str(math.floor(((etctime)/60)%60)) + "m " + 
                str("{:.0f}".format(etctime%60)) + "s]"
             )
    # ベイク終了時frame_currentで作業前のフレームを返すので
    # 0 == (frame_current - scene.frame_start) であれば最後のフレームとみなす
    elif g_total_time and 0 == ((scene.frame_current - scene.frame_start)):
        difference_time = current_time - previous_time
        g_total_time += difference_time.total_seconds()
        etctime = ((g_total_time / (scene.frame_current - scene.frame_start)) * (scene.frame_end - scene.frame_current))
        print(
                str("{:.1f}".format(difference_time.total_seconds())) + " sec " + 
                str("(avg {:.1f}".format((g_total_time / (scene.frame_end)))) + "s) / [" + 
                str(math.floor((g_total_time)/3600)) + "h " + 
                str(math.floor(((g_total_time)/60)%60)) + "m " + 
                str("{:.0f}".format(g_total_time%60)) + "s] " + 
                str("-----> {:.1f}".format(((scene.frame_end) / scene.frame_end)*100)) + "% " +
                "-----> ETC:[" +
                str(math.floor((etctime)/3600)) + "h " + 
                str(math.floor(((etctime)/60)%60)) + "m " + 
                str("{:.0f}".format(etctime%60)) + "s]"
             )

# 毎フレーム移動前に呼び出すハンドラ
bpy.app.handlers.frame_change_pre.append(my_handler)

# 全シミュレーションベイク
print("--- physics bake start ---")
bpy.ops.ptcache.bake_all(bake=True)
print("---- physics bake end ----")

# 処理が終わったら上書き保存します
bpy.ops.wm.save_mainfile()
```

## 注意点
bpy.ops.ptcache.bake_all　でのベイク時の開始・終了フレームを取得する方法が無い様なので
（やるとしても全オブジェクトのプロパティを走査するとかになりそう）
再生フレーム範囲==ベイクフレーム範囲では無い場合
処理自体は止まりませんが残り時間などの表記がおかしくなります。

## 謝辞
Blender Artists
https://blenderartists.org/t/how-to-display-processing-performance-in-cui-physics-bake/1453569