自作レンダラーを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本体で、改造なしにできる、レンダラー組み込み方法を見ていきます。
まず、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++などのレンダラーから行うことを考えます。
----------------------
データの引き渡し
自作レンダラーに引き渡さないといけません。
これを行うには複数の方法が考えられます
- (1) 自作レンダラーのPythonインタフェースをboost.pythonやpybind11等で作り、ダイナミックリンクさせ、Blenderと同一プロセスで自作レンダラーを動かす。
- (2) TCPやwebsocketやnamedpipeでローカル通信を行う。
- (3) メモリマップファイルを使用して一部のメモリを共有させる。
(2)を行うことで、OctaneのようにGPLライセンスの汚染を回避できたりしますが、
今回は最近やってみたかった(3)のメモリマップファイルというのを使ってみることにします。
----------------------
メモリマップファイルで画像共有
マルチプロセスの環境では、直接メモリを参照できる(と思われる)メモリマップドファイルは、かなり有効な手段ではないかと思い、試してみます。
stbimageで1枚画像を読み込んで、メモリマップとして登録するC++コードはこんな感じです。
続いて、python側でこれを表示させます。
とりあえずVCのデバッグで、UnmapViewOfFileのところでbreakで止めて
render_sceneのところに以下のコードを埋めこんでレンダーしてみます。
画像サイズはとりあえず決め打ちです。

ふむ、逆になってますが、なんか出ました。結果画像はいけそうです。
----------------------
objで出力するコード部分にメモリマップで出すように仕込んだりしてみましたが、
objで出力する(Blender標準の)エクスポーターが遅い!!!
100万ポリゴン一瞬で出してくれないと困ります。
BlenderはAlembicに対応しましたが、Alembicの出力コードはpythonからは全くアクセスできず、
こちらも使えません。
また、各種自作レンダラーは、独自のシーンファイルを定義しないと動かなかったりして、なかなか厄介です。
今回は最近やってみたかった(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からは全くアクセスできず、
こちらも使えません。
また、各種自作レンダラーは、独自のシーンファイルを定義しないと動かなかったりして、なかなか厄介です。