Blender

カメラの向きを操作しよう

BlenderをPythonで操作するシリーズ第4回です。

前回はこちら → Blenderのカメラを操作しよう

前回はカメラの位置を操作したり、クリッピング面について勉強したりしました。今回はカメラの撮影方向の制御方法と、カメラの注視点を指定して適切な角度を算出する方法について説明します。

※ 2019/08/17 追記
記事の内容をBlender 2.80に対応させました。


Blenderの座標系

前回特に触れませんでしたが、3D空間上で座標を扱う時はその座標がどの座標系でのものなのかを意識する必要があります。座標系にはワールド座標系ローカル座標系がありますが、詳しい説明はここでは割愛します。

また、座標軸にも注意が必要です。手をフレミングの左手の法則の形にして、親指を \(x\) 軸、人差し指を \(y\) 軸、中指を \(z\) 軸と見立てた時に、右手と左手のどちらの手の形と合うかどうかで、右手系左手系とそれぞれ名前がついています。

さらに \(y\) 軸を高さ方向に取るのか、\(z\) 軸を高さ方向に取るのかで、Y-upやZ-upと呼び方が変わります。

確認してもらえれば分かりますが、BlenderはZ-upの右手座標系になります。


カメラを回転させる

前回、カメラの位置を変更させる方法を説明しましたが、より柔軟にレンダリングするためには回転動作が必要不可欠です。

カメラを回転させるにはbpy.data.objectsから取得できるカメラオブジェクトの、rotation_eulerプロパティに値をセットすれば良いです。セットする値はXYZオイラー角を表す要素数3のリストになります。リストでなくても3つの浮動小数点値のセットなら大丈夫かもしれません。タプルとndarrayは大丈夫なのを確認しました。

rotation_modeプロパティの値を変更することで、別の系のオイラー角で指定することができます。このプロパティの初期値はXYZで、XYZオイラー角を意味します。オイラー角の説明はここでは割愛します。


カメラを注視点で制御する

カメラを回転させられるようになったので、自由自在にいろいろな場所をレンダリングできるようになったわけですが、どこか特定の位置座標を中心に捉えたい時に角度で制御するのは大変そうですよね。

そこで、中心に捉えたい点のワールド位置座標と現在のカメラのワールド座標から、rotation_eulerプロパティにセットするべき値を計算する方法を考えてみます。

そのためにはまず、全て \(0^\circ\) の状態を確認して、それぞれの軸の回転がどの軸を基準にしているか確認する必要があります。

Blenderを起動してカメラを選択し、右側にあるコントロールパネルの内の以下の画像の赤枠の部分を変更することで、回転の角度を変更することができるので、色々値を変えてどの軸の回転によってカメラがどの様に変わるのか確認してみて下さい。

rotation_props
カメラオブジェクト選択時のプロパティの一部

確認できましたか?確認できましたら、注視点の位置座標がカメラを原点にしたローカル座標系で \((X, Y, Z)\) である時、以下のような画像の状態にあることが分かると思います。

注視点 \((X,Y,Z)\) を中心に捉えたカメラの各状態を表す図。
\(\alpha\) は \(x\) 軸周りの回転角度を、\(\gamma\) は \(z\) 軸周りの回転角度をそれぞれ表す。

ここで、\(\alpha\) と \(\gamma\) をそれぞれどの様に計算すればよいか考えていきましょう。

まずは \(\alpha\) から考えていきます。以下のように \(z\) 軸と注視点へのベクトルが作る平面を抜き出して考えます。

\(\alpha\) に関係する平面。小豆色の \(x\) 軸と若緑色の \(y\) 軸はそれぞれ、
\(\alpha\) をarctan2を用いて計算しようとする時に対応する軸の方向を表しています。

ここで、\(\alpha\) をarctan2()を用いて計算しようとすると、\(\alpha\) は \(z\) 軸を \(0^\circ\) の基準として時計回りに正の方向となっているので、arctan2に渡す引数の値に対応する軸はそれぞれ、図中の小豆色の \(x\) 軸と、若緑色の \(y\) 軸と見なせます。

従って、引数として渡すyxにはそれぞれ以下の値を渡せば良いことが分かります。

\[
\begin{align}
y &= \sqrt{X^2 + Y^2} \\
x &= -Z
\end{align}
\]

\(x\) 軸と \(z\) 軸の向きが逆になっていることで、注視点の \(z\) 座標の値を \(-1\)倍して渡していることに注意して下さい。

同じ様に今度は \(\gamma\) を計算する方法を考えます。考える必要があるのは \(x-y\) 平面です。

\(\gamma\) に関係する平面。小豆色の \(x\) 軸と若緑色の \(y\) 軸はそれぞれ、
\(\gamma\) をarctan2を用いて計算しようとする時に対応する軸の方向を表しています。

\(\gamma\) の \(0^\circ\) の基準となるのは \(y\) 軸で、こちらも時計回りに正の方向なので、arctan2に渡す引数の値に対応する軸はそれぞれ、図中の小豆色の \(x\) 軸と、若緑色の \(y\) 軸と見なせます。

従って、引数として渡すyxにはそれぞれ以下の値を渡せば良いことが分かります。

\[
\begin{align}
y &= -X \\
x &= Y
\end{align}
\]

以上で \(\alpha\) と \(\gamma\) を計算するための式は揃いましたが、私はnumpyで一度に計算するために以下のようにベクトル形式に纏めました。

\[
\left[
\begin{array}{c}
\alpha \\
0 \\
\gamma
\end{array}
\right] = \arctan \left(
\left[
\begin{array}{c}
|(X, Y, 0)| \\
0 \\
-X
\end{array}
\right], \left[
\begin{array}{c}
-Z \\
+0 \\
Y
\end{array}
\right]
\right)
\]

\(|(X, Y, 0)|\) は 位置ベクトル \((X, Y, 0)\) の大きさを表しています。

以上を踏まえて以下のようなプログラムを書いてみました。

import bpy
import numpy as np
import numpy.linalg as LA

locations = [(5, 5, 5),
             (-5, 5, 5),
             (-5, -5, 5),
             (5, -5, 5),
             (5, 5, -5),
             (-5, 5, -5),
             (-5, -5, -5),
             (5, -5, -5)]
camera = bpy.data.objects['Camera']


def look_at(target):
    ray = np.subtract(target, camera.location)
    ray_xy = np.array([ray[0], ray[1], 0])
    x = np.array([-ray[2], +0, ray[1]])
    y = np.array([LA.norm(ray_xy), 0, -ray[0]])
    camera.rotation_euler = np.arctan2(y, x)


def render(filepath='img/', filename='look_at.png'):
    bpy.ops.render.render()
    bpy.data.images['Render Result'].save_render(filepath + filename)


look_at_point = [0, 0, 0]
for i, location in enumerate(locations):
    camera.location = location
    look_at(look_at_point)
    render(filename=f'render{i}.png')

16行目から21行目までが今回の肝になる部分です。17行目で注視点のカメラのローカル座標系での座標を計算し、21行目で \(\alpha\) と \(\gamma\) を計算してrotation_eulerプロパティに設定しています。

このコードを実行すると、5行目から12行目で定義している8種類の位置から、座標 \((0, 0, 0)\) に注目した画像をレンダリングします。画像を全て貼るのは大変なので、ぜひご自分の環境で試してみて下さい。


レンダリングの解像度

これまで何度か画像をレンダリングしてきましたが、生成された画像の解像度の確認をした方は居ますか?確認してもらえれば分かりますが、恐らく \(960 \times 540\) になっていると思います。

※ 2019/08/17 追記
Blender 2.80から以下で説明するresolution_percentageプロパティのデフォルト値が100になりました。従って特に設定することなく標準で生成される画像は \(1920 \times 1080\) になります。

では、カメラの設定値がこれらの値になっているかというと、そういう訳でもないようです。

import bpy

render_settings = bpy.data.scenes['Scene'].render
print(render_settings.resolution_x)
print(render_settings.resolution_y)

上のコードを試すと、Full HDである \(1920 \times 1080\) になっていることが確認できると思います。

実は、上記コードのrender_settingsに含まれているresolution_percentageプロパティの値によって、レンダリングの解像度が半分にされていました。

従って、以下のコードをレンダリング前に実行することで、設定した解像度でレンダリングできるようになります。

bpy.data.scenes['Scene'].render.resolution_percentage = 100

今回は以上になります。

実はBlenderのカメラには、指定したオブジェクトをトラッキングする機能があるので、それを使えばカメラの注視点を手動で操作する必要は無くなるのですが、オブジェクトが無い空間のどこかを注視点にしたい時は自分で角度を計算したりする必要があります。そのような際に、今回紹介した方法を活用してもらえれば良いかなと思います。

次回は光源について説明します。