Blender でキーフレームごとに出力解像度を設定する方法

![2020-11-19 00_11_40-StrokesPlus.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8267/549a12fa-cbf1-8fe3-fb08-b7244b61b3ba.jpeg)

漫画の下書きに、 Blender のレンダー画像を使うことになりました。「1コマ＝1キーフレーム」と対応させ、キーフレームごとのレンダー画像を漫画作成ソフトに貼り付けて使う想定です。

通常、漫画のコマは、1コマごとに縦横比が異なります。しかし、Blender ではキーフレームごとに解像度を設定することができません。すべてのキーフレームで同じ解像度にするか、1フレームずつ手動で切り替えるしかありません。どちらも不便です。

そこで、キーフレームごとに解像度を設定する方法を考えました。 Blender にはキーフレームに **マーカー** という文字列を設定できるので、そこに解像度の縦横比を記録することにしました。

なお、一般的な横読み漫画ではコマの縦横のサイズは不定ですが、今回は縦読み漫画であり、コマの横幅が常に 2480px で一定という想定です。縦横を同時に指定する方法は、末尾で補足しておきます。



実行環境
----

* Blender 2.90.1


やりたいこと
-----

![screenshot.1596102429.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/8267/549e37e9-003d-19c2-1fa2-4af1cf46dcd9.png)


* "Scene > Dimensions > Resolution X&Y" を、キーフレームごとに設定・保持できるようにしたい
* X は 2480px 固定で、Y はキーフレームに設定された解像度比の値に応じて変更したい
* 例えば上の画像では、キーフレーム 47 に設定された縦横比は 1.5 なので、`Y = 2480 * 1.5 =  3720 px` になる


実現方針
----

* 「キーフレームごとに設定された解像度比の値を参照し、自動で Resolution X&Y を変更する」 という Python スクリプトを作成する
* 解像度比はマーカーに数値で記録する



設定手順
----

### マーカーに縦横比を入力する

Timeline パネルで、[M] キーでキーフレームにマーカーを追加し、[Ctrl+M] でマーカー名として縦横比を入力します。

今回は `Y/X` の数値を入力してください。例えば X:Y = 4:3 なら `0.75`, 1:1 なら `1`, 1:2 なら `2` です。

ちなみに1フレームに複数のマーカーが設定できますが、今回のプログラムでは最初に見つかった数値を縦横比として採用します。


### Python スクリプトを書く

次に Text Editor パネルを開き、以下のコードを入力してください。

```python:set_resolution_from_marker.py
import bpy

RESOLUTION_X = 2480
DEFAULT_RATIO = 1.0


def find_ratio_from_current_keyframe():
    # 全マーカーの中から、現在のキーフレームに設定されているマーカーを探す
    marker_items =  bpy.context.scene.timeline_markers.items()
    current = bpy.data.scenes["Scene"].frame_current
    markers = [item[0] for item in marker_items if item[1].frame == current]
    
    # マーカーが存在しなければデフォルト比率を返す
    if len(markers) < 1:
        print(f"No marker is set to current keyframe. Now ratio is {DEFAULT_RATIO}.")
        return DEFAULT_RATIO

    print("Found marker(s): ", markers)
    # マーカーの文字列を数値に変換して返す
    for m in markers:
        try:
            ratio = float(m)
            print("Ratio is set to ", ratio)
            return ratio
        except ValueError as e:
            print(e)
            continue

    # 数値に変換できるマーカーが無かった場合、デフォルト比率を返す        
    print(f"Marker is set to current keyframe but not valid number. Now ratio is {DEFAULT_RATIO}.")
    return DEFAULT_RATIO


def update_resolution(scene):
    scene.render.resolution_x = RESOLUTION_X
    scene.render.resolution_y = RESOLUTION_X * find_ratio_from_current_keyframe()
    

# キーフレーム変更時に解像度を更新するよう、コールバックを設定
bpy.app.handlers.frame_change_pre.append(update_resolution)
```


ℹ️ 簡単な解説

* 「現在のキーフレームにあるマーカーだけを取得する」というAPIがなかったので、タイムライン全体のマーカーを一度全部取得し、その中から現在のキーフレームのマーカーだけを抽出しています。
* 現在のキーフレームにマーカーが設定されてない、または有効な数値が入力されてない場合、 `DEFAULT_RATIO` という定数を返すようにしています。今回は `1` (=縦横比 1:1)です。
* 最後の行では、キーフレームが変更されるたびに `update_resolution()`を呼び出して解像度を変更するよう、コールバックを設定しています。
  * 参考:  [Application Handlers \(bpy\.app\.handlers\) — Blender Python API](https://docs.blender.org/api/current/bpy.app.handlers.html)


### スクリプトを実行する

Text Editor パネルで "Run Script (Alt+P)" を実行してください。

あとは、キーフレームを切り替えるたび、Resolution X&Y が自動的に設定されます。






補足
----

### Resolution X&Y どちらも設定可能にしたい場合は？

今回は「X=2480px 固定」というやや特殊なケースでしたが、X,Y を同時に設定したければ、マーカーの表記と Python スクリプトを変更すれば簡単に対応できます。

たとえばマーカーに `1280,720` と書くようにし、Python スクリプトでカンマ区切りで文字列を分割して、1つ目の値を X, 2つ目の値を Y に代入するなど。


### ドライバーで実現できないの？

できませんでした。Resolution を含め、Scene プロパティー配下の値にはどれもドライバーが設定できないためです。


### マーカーを入力・変更した時に解像度が更新されないんだけど？

そのタイミングで呼び出せるコールバックが見当たりませんでした:disappointed:
お手数ですが、矢印キーでキーフレームを1つ進めて戻してください。



参考リンク
----

- [Add driver to render resolution - Support / Animation and Rigging - Blender Artists Community](https://blenderartists.org/t/add-driver-to-render-resolution/601348/6)
- [Application Handlers (bpy.app.handlers) --- Blender 2.61.4 - API documentation](https://docs.blender.org/api/blender_python_api_2_61_4/bpy.app.handlers.html?highlight=bpy.app.handlers#bpy.app.handlers.persistent)
- [TimelineMarker(bpy_struct) --- Blender Python API](https://docs.blender.org/api/current/bpy.types.TimelineMarker.html)
- [TimelineMarkers(bpy_struct) --- Blender Python API](https://docs.blender.org/api/current/bpy.types.TimelineMarkers.html)
- <https://docs.blender.org/api/current/bpy.types.Scene.html#bpy.types.Scene.frame_current>