2016年12月11日日曜日

Custom Renderer(1)

本記事は、レイトレ Advent Calender 2016 10日目の記事です。


自作レンダラーをBlenderで使うためのあれこれです。
レイトレ自体とはあまり関係ないので恐縮です。

やりたいこと



前提として、この記事ではGPLライセンスは無視します。
成果物を公開する必要がない、Blenderを実験用DCCツールとして使いたい
といったプログラマを想定しています。

------------

事前知識

BlenderにはCyclesというパストレのレンダラーが載っています。
このレンダラーが、モンテカルロ的な感じで、良い感じにジワジワとリアルな絵を
レンダリングしてくれるのはご存知かと思います。

Cylcesはapacheライセンスで、オープンソースで公開されています。
レンダラーを作ってる人は、無料のBlenderに乗ってるCyclesみたいな感じで
本体に自分のレンダラーを組み込んでテストしたい!と思うはずです。

それを実際に商用ソフトで実践した例としてOctaneRender(for Blender)があります。
こちらは、まさにCyclesのようにノードを組み、シーンによりますがCyclesより速い速度で
Blenderのレンダービューにレンダリング結果を表示します。



OctaneRenderでは、Blender本体を改造して、TCP通信によりローカルにインストールされたOctaneRenderServerと高速に通信を行っています。
一見リアルタイムに見えますが、裏でTCPでジオメトリやカメラ情報を送り、
送り返されたレンダリング結果画像をBlender上に表示しているというわけです。
通信を介することで、GPLライセンスの汚染を回避しています。

このOctaneRender(forBlender)による仕組みを利用できないか、と考えたこともありました
しかし、このアプローチは本体改造のため、延々に本体の更新に合わせて
サポートし続ける必要があり、趣味で週末にやるには厳しいものがあります。

そこで、Blender本体で、改造なしにできる、レンダラー組み込み方法を見ていきます。

レンダラ用API

まず、Blender PythonでRender Engineというのがあります。
RenderEngine(bpy_struct)
https://www.blender.org/api/blender_python_api_2_78a_release/bpy.types.RenderEngine.html?highlight=renderengine

こちらにあるサンプルをBlenderのTextEditorに張り付けて実行します。
すると、Flat Color Rendererというレンダラーが追加されます。





続いて、F12を押してレンダリングしてみます。



このように青くなります。
先ほど張り付けたスクリプトでは、次のあたりで画像作ってるようです。

    # In this example, we fill the full renders with a flat blue color.
    def render_scene(self, scene):
        pixel_count = self.size_x * self.size_y
        # The framebuffer is defined as a list of pixels, each pixel
        # itself being a list of R,G,B,A values
        blue_rect = [[0.0, 0.0, 1.0, 1.0]] * pixel_count 
        # Here we write the pixel values to the RenderResult
        result = self.begin_result(0, 0, self.size_x, self.size_y)
        layer = result.layers[0].passes["Combined"]
        layer.rect = blue_rect
        self.end_result(result)

ピクセルを書き込んでるようですね。無事画像は出せそうです!

とは言っても、Pythonでレンダラー作ってる人なんて少数派なので、
これを別プロセスで走るC++などのレンダラーから行うことを考えます。

----------------------

データの引き渡し

レンダリングするには、Blender上のジオメトリやテクスチャ、カメラやノードなどのデータを、
自作レンダラーに引き渡さないといけません。

これを行うには複数の方法が考えられます
  • (1) 自作レンダラーのPythonインタフェースをboost.pythonやpybind11等で作り、ダイナミックリンクさせ、Blenderと同一プロセスで自作レンダラーを動かす。
  • (2) TCPやwebsocketやnamedpipeでローカル通信を行う。
  • (3) メモリマップファイルを使用して一部のメモリを共有させる。
(2)を行うことで、OctaneのようにGPLライセンスの汚染を回避できたりしますが、
今回は最近やってみたかった(3)のメモリマップファイルというのを使ってみることにします。


----------------------

メモリマップファイルで画像共有

マルチプロセスの環境では、直接メモリを参照できる(と思われる)メモリマップドファイルは、かなり有効な手段ではないかと思い、試してみます。

stbimageで1枚画像を読み込んで、メモリマップとして登録するC++コードはこんな感じです。

#define STB_IMAGE_IMPLEMENTATION

#include <windows.h>
#include <string>
#include "stb_image.h"

int main(int argc, char *argv[])
{
 if (argc <= 1) return -1;

 std::string file = argv[1];
 int w, h, channels;
 unsigned char* data = stbi_load(argv[1], &w, &h, &channels, 4);

 HANDLE hmap = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE, 0, w * h * 4, "testmmap");
 if (hmap == NULL) {
  stbi_image_free(data);
  return -1;
 }
 if (GetLastError() == ERROR_ALREADY_EXISTS) {
  printf("ERROR_ALREADY_EXISTS\n");
 }
 LPSTR mapview = (LPSTR)MapViewOfFile(hmap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
 if (mapview == NULL) {
  stbi_image_free(data);
  return -1;
 }

 // メモリマップに画像を転送.
 memcpy(mapview, data, w * h * 4);

 // TODO: ここでメモリマップドファイルをBlenderで読み込み表示させる

 // ここでメモリマップ削除される.
 UnmapViewOfFile(mapview);

 CloseHandle(hmap);
 stbi_image_free(data);
 return 0;
}

続いて、python側でこれを表示させます。
とりあえずVCのデバッグで、UnmapViewOfFileのところでbreakで止めて
render_sceneのところに以下のコードを埋めこんでレンダーしてみます。
画像サイズはとりあえず決め打ちです。


        blue_rect = [[0 for i in range(4)] for j in range(pixel_count)]
        
        mm = mmap.mmap(-1, 240*240*4, tagname="testmmap")
        mm.seek(0, os.SEEK_END)
        mmsize = mm.tell()
        mm.seek(0)
        image = mm.read(mmsize)

        for i in range(int(mmsize / 4)):
            rgba = struct.unpack_from("4B", image, i*4)
            blue_rect[i][0] = rgba[0] / 255
            blue_rect[i][1] = rgba[1] / 255
            blue_rect[i][2] = rgba[2] / 255
            blue_rect[i][3] = rgba[3] / 255





ふむ、逆になってますが、なんか出ました。結果画像はいけそうです。


----------------------

データ共有

これが厄介です。

objで出力するコード部分にメモリマップで出すように仕込んだりしてみましたが、
objで出力する(Blender標準の)エクスポーターが遅い!!!
100万ポリゴン一瞬で出してくれないと困ります。

BlenderはAlembicに対応しましたが、Alembicの出力コードはpythonからは全くアクセスできず、
こちらも使えません。

また、各種自作レンダラーは、独自のシーンファイルを定義しないと動かなかったりして、なかなか厄介です。


すみませんここ考えてる途中で時間切れです!(続く)

明日は Ushio さんによる、”確率密度関数周りの何かを” です!