最新物理ベースレンダラー Mitsuba2を触ってみる (2) Pythonから動かす編

# Mitsuba2とは
アカデミック向けのフリーの物理ベースレンダラー。微分可能レンダリングや偏光レンダリングなどの新機能がある。
この記事は[導入編](https://qiita.com/kurilab/items/34089636fa570d9a7939)の続きになります。
Mitsuba2のインストールまで終わっているという前提です。

他に書いた関連記事です。

 - [最新物理ベースレンダラー Mitsuba2を触ってみる(1) 導入編](https://qiita.com/kurilab/items/34089636fa570d9a7939)
 - [最新物理ベースレンダラー Mitsuba2を触ってみる(3) 微分可能レンダリング編](https://qiita.com/kurilab/items/5f270ebf8ee28a25cd92)
 - [最新物理ベースレンダラー Mitsuba2を触ってみる(4) 偏光レンダリング編](https://qiita.com/kurilab/items/c96ee592dbde0416576e)
 - [BRDFと偏光BRDF(pBRDF)の違い](https://qiita.com/kurilab/items/f9e48045bd1b9c130d0b)

# PythonからMitsuba2を動かす
[公式でも書かれているように](https://mitsuba2.readthedocs.io/en/latest/src/python_interface/intro/)Mitsuba2は非常に強力なPythonバインディングを提供していて、ほぼすべての機能をPythonから使うことが可能です。もちろんJupyter Notebook上でimportして、様々な開発をインタラクティブに行うこともできます。更にはPyTorchと連携することも想定していて、とても簡単にPyTorchを使った最適化が記述できます。

## まずは何よりも動かしてみる
**Windows10での手順になります（2020年3月10時点での情報）。**
公式がPythonのレンダリングテストコードを用意してくれています、これを動かせるようになるのが目標です。
今回は[導入編(1)](https://qiita.com/kurilab/items/34089636fa570d9a7939)が終わっている人ならば、特に引っ掛かるところはないと思います。手順を追って解説します。

## Pythonの対応バージョン
[公式によると](https://mitsuba2.readthedocs.io/en/latest/src/getting_started/compiling/)3.6以上に対応しているとの事なので、それ未満の場合はアップデートしておきましょう。

## PATHを通す
まず、Pythonでimportできるようにするために、mitsuba2のモジュールにPATHを通す必要があります。
これにはいくつかやり方があるのですが、ここでは公式が用意しているスクリプトを使うことにします。
[導入編(1)](https://qiita.com/kurilab/items/34089636fa570d9a7939)でビルドしたライブラリや実行ファイルが`mitsuba2\build\dist\`にあることが前提です。
ルートディレクトリに`setpath.bat`というバッチファイルがあるので、これをコマンドプロンプト(cmd)から叩きます（Power Shellを使うとbatを叩いても環境変数が返ってこないので、スクリプトを書き直す必要があります）。

```
mitsuba2> setpath.bat
```

これで「実行したcmd内では」実行ファイルやPythonモジュールへのPATHが通り、Mitsuba2がどこでも使える状態になります。
試しにipythonを起動して、mitsuba2をimportしてみましょう。

```
mitsuba2> ipython
Python 3.7.4 (tags/v3.7.4:e09359112e, Jul  8 2019, 20:34:20) [MSC v.1916 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 7.10.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import mitsuba
```
importエラーが出なければ、mitsuba2モジュールへのPATHが通っています。
**mitsuba2ですが、importするモジュールは「mitsuba」であることに注意してください。**

さて、では早速テストコードを動かしてみましょう。
「スクリプトを実行したcmd」で、公式が用意しているサンプルコードのディレクトリまで移動します。
該当ディレクトリは`mitsuba2\docs\examples\01_render_scene\`にあります。

```
mitsuba2> cd docs\examples\01_render_scene\
mitsuba2\docs\examples\01_render_scene>
```

ディレクトリ内にある`render_scene.py`がPythonでmitsuba2を叩いてレンダリングを行うサンプルコードです。
適当なエディタで開いてみましょう。

```python

import os
import numpy as np
import mitsuba

# Set the desired mitsuba variant
mitsuba.set_variant('scalar_rgb')

from mitsuba.core import Bitmap, Struct, Thread
from mitsuba.core.xml import load_file

# Absolute or relative path to the XML file
filename = 'path/to/my/scene.xml'

# Add the scene directory to the FileResolver's search path
Thread.thread().file_resolver().append(os.path.dirname(filename))

# Load the actual scene
scene = load_file(filename)

# Call the scene's integrator to render the loaded scene
scene.integrator().render(scene, scene.sensors()[0])

# After rendering, the rendered data is stored in the film
film = scene.sensors()[0].film()

# Write out rendering as high dynamic range OpenEXR file
film.set_destination_file('/path/to/output.exr')
film.develop()

# Write out a tonemapped JPG of the same rendering
bmp = film.bitmap(raw=True)
bmp.convert(Bitmap.PixelFormat.RGB, Struct.Type.UInt8, srgb_gamma=True).write('/path/to/output.jpg')

# Get linear pixel values as a numpy array for further processing
bmp_linear_rgb = bmp.convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, srgb_gamma=False)
image_np = np.array(bmp_linear_rgb)
print(image_np.shape)

```

**まずはコードの理解をする前に、動かします。**
今のままではレンダリング対象のシーンファイルが存在しないので、シーンファイルを用意します。導入編(1)でレンダリングテストを行った方は既にあると思うのですが、サンプルのシーンデータのリポジトリをどこか別の場所でgit cloneします。

```bash
git clone https://github.com/mitsuba-renderer/mitsuba-data.git
```

ここのローカルリポジトリのパスを指定しても良いのですが、わかりやすくするために、今回も`mitsuba_data/scenes/cbox`をディレクトリごと、現在のテストコードのディレクトリ`mitsuba2\docs\examples\01_render_scene\`にコピーしてください。

そして、コード内でこのシーンファイルを読み込み、適切な場所に書き出すために3か所だけ書き換えます。

```python
# filename = 'path/to/my/scene.xml'
filename = 'cbox/cbox.xml' # l.12
```

```python
# film.set_destination_file('/path/to/output.exr')
film.set_destination_file('cbox/output.exr') # l.27
```

```python
# bmp.convert(Bitmap.PixelFormat.RGB, Struct.Type.UInt8, srgb_gamma=True).write('/path/to/output.jpg')
bmp.convert(Bitmap.PixelFormat.RGB, Struct.Type.UInt8, srgb_gamma=True).write('cbox/output.jpg') # l.32
```

あとは、pythonで実行するだけです。（numpyはpip installしておいてください）

```bash
python render_scene.py
```

無事レンダリングが終わると、`cbox\output.exr`と`cbox\output.jpg`という2つのファイルが生成されています。
exrは導入編(1)と同じで32bit x 3chのレンダリング画像、jpgは8bit x 3chの圧縮画像になります。
![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/215987/adc231aa-65e3-d353-2ad6-b3f27ad34134.jpeg)

# サンプルコードを理解する
さて、無事サンプルコードを動かし終わったので、１つ１つコードを順を追って理解していきます。

```python
import os
import numpy as np
import mitsuba
```
モジュールをimportしています、特に変わったところはないです。


```python
# Set the desired mitsuba variant
mitsuba.set_variant('scalar_rgb')
```

mitsuba2のバリアント（レンダリング設定）を指定します。[バリアントについては導入編(1)で解説しています](https://qiita.com/kurilab/items/34089636fa570d9a7939)。
**これは、CMake時に指定したものの中から選ばないといけません。**もしビルドしていない他のバリアントでレンダリングしたい場合はCMakeまで遡ってビルドし直す必要があります。今回は"scalar_rgb"という、SIMD化もない(scalar)、スペクトルと偏光を考慮しないRGBの光線(rgb)という一番基本のバリアントでレンダリングを行います。

```python
from mitsuba.core import Bitmap, Struct, Thread
from mitsuba.core.xml import load_file
```

`mitsuba.core`から、`Bitmap, Struct, Thread`をimportします。
また`mitsuba.core.xml`から、`load_file`をimportします。
それぞれ後で使う際に解説します。

```python
# Absolute or relative path to the XML file
filename = 'cbox/cbox.xml'

# Add the scene directory to the FileResolver's search path
Thread.thread().file_resolver().append(os.path.dirname(filename))

# Load the actual scene
scene = load_file(filename)
```

読み込むxml形式のシーンファイル名を指定して、シーンファイルを読み込む関数`load_file()`で読み込みます。
`Thread.thread().file_resolver()`は、名前やパスを指定して検索パスとして設定することで、シーンファイル内のファイルへのリンクが相対パスで書かれていることを解決します。これが無いとオブジェクトファイルが見つかりませんというエラーがでます。

```python
# Call the scene's integrator to render the loaded scene
scene.integrator().render(scene, scene.sensors()[0])
```

読み込んだシーンをレンダリングするためのインテグレーター(`integrator`)を呼び出します。
第一引数がシーン(`scene`)で、第二引数がレンダリングをする（シーンファイルに記述されている）センサー(`scene.sensors()[0]`)です。[0]なのは、センサーがいくつかある場合、どれかを指定できるようにするためです。


```python
# After rendering, the rendered data is stored in the film
film = scene.sensors()[0].film()

# Write out rendering as high dynamic range OpenEXR file
film.set_destination_file('cbox/output.exr')
film.develop()
```

シーンファイルに記述されているセンサーに対するフィルム(`scene.sensors()[0].film()`)を読み込みます。
フィルムは、データの出力設定や後処理などをする役目があります。
`film.set_destination_file`で出力ファイル名を設定します。
`film.develop()`で`cbox\output.ext`を現像します。

```python
# Write out a tonemapped JPG of the same rendering
bmp = film.bitmap(raw=True)
bmp.convert(Bitmap.PixelFormat.RGB, Struct.Type.UInt8, srgb_gamma=True).write('cbox/output.jpg')

```

8bit現像処理になります。
`film.bitmap(raw=True)`で事前にフィルム現像されたコンテンツを保存するビットマップオブジェクトを返します。**引数`raw`の意味は、公式でもまだ記述されていないので不明。**
さらにビットマップオブジェクトに対し`convert`で8bit現像処理を行います。`Bitmap.PixelFormat.RGB`でRGBを指定、`Struct.Type.UInt8`で8bit指定、`srgb_gamma=True`でSRGBのGammaのトーンマップを適用します。


```python
# Get linear pixel values as a numpy array for further processing
bmp_linear_rgb = bmp.convert(Bitmap.PixelFormat.RGB, Struct.Type.Float32, srgb_gamma=False)
image_np = np.array(bmp_linear_rgb)
print(image_np.shape)
# (256, 256, 3)
```
同じ`convert`で、8bit圧縮されていない、線形のRGB値をnumpy arrayで取得しています。
出力結果とは関係ないですが、**レンダリング結果が容易にnumpy arrayで取得できることがわかります。**

# まとめ
PythonからMitsuba2を動かす方法について、サンプルコードを動かし、そのコードの中身を見て理解していきました。
とてもシンプルな記述で、基本のレンダリングをPythonで動かせることがわかっていただけたかと思います。
次回からはいよいよ微分可能レンダリングや偏光レンダリングなど新機能の使い方を解説します。

 - [最新物理ベースレンダラー Mitsuba2を触ってみる(3) 微分可能レンダリング編](https://qiita.com/kurilab/items/5f270ebf8ee28a25cd92)
 - [最新物理ベースレンダラー Mitsuba2を触ってみる(4) 偏光レンダリング編](https://qiita.com/kurilab/items/c96ee592dbde0416576e)

以上です。