Embree入門

著者:久保 尋之
(公開:2017/09/02,更新:2019/12/18)

EmbreeはIntelが提供するCPUベースのハイパフォーマンスレイトレーサです.シーン中に膨大な数の三角形があるとき,あるレイがどの三角形とどこで交差するのか計算(交差判定と呼びます)するためには,非常に大きな計算コストが掛かってしまいます.これを効率的に計算する手法はいろいろ開発されているのですが,自分で実装するのは大変です.Embreeは交差判定を良い感じにやってくれる,非常に有り難いライブラリです.また,環境が整っていれば内部でSSE,AVXなどの命令も使って高速化してくれているそうです.とっても便利なライブラリですが,日本語のドキュメントがあまり多くないようだったので,簡単な解説をしたいと思っています.なお,Embreeはレンダリングそのもの,つまり各画素の輝度を決める仕組みは入っていないので,パストレーシングなりフォトンマッピングなり,輝度計算の手法を自分で実装する必要があります.

Embreeのインストール

本稿では64bit版Windows10とVisual Studio2015を使ってプログラムを作ってみます.なお,執筆時のv2.16.5ではOSはWindows32bit, 64bit,Linux64bit,Mac OSX64bitに,コンパイラはIntel Compiler, GCC, Clang,Microsoft Compiler (vcc)対応しているそうです.Intel Compilerを使うと10%程度のパフォーマンス改善が見られるそうですが,残念ながら手元にないので諦めてVisual Studioを使います.

作業用フォルダを準備

今回,Embreeを使ってみるにあたり,ダウンロードしてきたライブラリや自分で作成するソースコードなどを保存するフォルダを作ります.どこに作っても構いませんが,差し当たってCドライブ直下に"embree"というフォルダを作成し,そこに保存していくものとします.

プリコンパイル済みライブラリをダウンロード

まずはEmbreeのDownloadページからコンパイル済みのライブラリをダウンロードします.インストーラ(.exe)とZip圧縮版(.zip)とがありますが,ここではzip圧縮版を使ってみましょう.本稿では64bit版Windowsを使いますので"embree-2.16.5.x64.windows.zip"をダウンロードして,C:\embreeに保存し,そのフォルダで解凍して下さい.なお,タイミングによってバージョンは変わりますので,ファイル名は適宜読み替えてください.

ソースコードのダウンロード

次に同じくDownloadページからソースコードをダウンロードします.zip圧縮版(*.zip)とtar.gz圧縮版(*.tar.gz)とがありますが,zip圧縮版を使ってみましょう."embree-2.16.5.zip"をダウンロードして,C:\embreeに保存し,そのフォルダで解凍して下さい.こちらも,タイミングによってバージョンは変わりますので,ファイル名は適宜読み替えてください.

ここまで終わると,C:\embreeフォルダの中は下図のようになっているはずです.

Embreeを使ってプログラミングしてみる

デバイスを作る

いよいよVisual Studio2015を使ってプログラムを作っていきます.Visual Studio2015を起動し,プロジェクトを新規作成する際に「Win32 コンソール アプリケーション」を選び,「空のプロジェクト」を作成してください.次に,「ビルド」-「構成マネージャ」からアクティブプラットフォームを「x64」に指定して下さい.
さらに,「プロジェクト」-「プロパティ」から,インクルードディレクトリ

  • C:\embree\embree-2.16.5
  • C:\embree\embree-2.16.5\include
  • C:\embree\embree-2.16.4.x64.windows\include
を追加します.

また,ライブラリディレクトリには

  • C:\embree\embree-2.16.4.x64.windows\lib
を追加してください.

さらに,「リンカ」-「追加の依存ファイル」

  • embree.lib
を追加して下さい.

また,C:\embree\embree-2.16.4.x64.windows\binに入っている

  • embree.dll
  • tbb.dll
プロジェクトファイルと同じディレクトリにコピーして下さい.
次に,下に示すソースコードをhello-embree.cppと名前を付けてプロジェクトに追加して下さい.

#include <iostream>
#include <embree2/rtcore.h>
#include <embree2/rtcore_ray.h>
int main(void)
{
	/* デバイスを作成 */
	RTCDevice device = rtcNewDevice(NULL);
	/* シーンを作成 */
	RTCScene scene = rtcDeviceNewScene(device, RTC_SCENE_STATIC, RTC_INTERSECT1);
	
	/* ここでこの後いろいろな処理を行う */
	/* .... */
	
	
	/* シーンを削除 */
	rtcDeleteScene(scene);
	/* デバイスを削除 */
	rtcDeleteDevice(device);
	return 0;
}

無事にコンパイルできたら,実行してみましょう.今回は単なる初期化と終了処理しかしていないため,特に何も出力されません.エラー無く実行できればそれでOKです.

RTCDevice rtcNewDevice(const char* cfg = NULL)
新しいEmbreeデバイスを作成します.Embreeを使うときには必ずデバイスを作る必要があります.cfgにはデバイスを作成するときの設定を渡すことができますが,NULLを入れるとデフォルトの設定が使われます.とりあえずNULLにしておけば良いでしょう.

void rtcDeleteDevice(RTCDevice device)
Embreeデバイスを削除します.プログラムの終了前には呼び出すようにします.

RTCScene rtcDeviceNewScene (RTCDevice device, RTCSceneFlags flags, RTCAlgorithmFlags aflags)
新しいシーンを作成します.シーンにはTriangleなど様々なジオメトリを格納することができますが,その詳細については後述します.第1引数には先に作成したデバイスを入れます.第2に引数にはシーンの種類を指定します.Embreeではdynamicシーンとstaticシーンの2種類をサポートしていますが,本稿ではしばらくはstaticシーンを使うことにしますので,RTC_SCENE_STATICを指定します.第3引数はアルゴリズムフラグと呼ばれていて,交差判定のモード(NormalモードとStreamモードがあります)や同時に扱うレイの数を指定できます.ここでは最も基本的なRTC_INTERSECT1を指定します.

void rtcDeleteScene (RTCScene scene)
シーンを削除します.プログラムの終了前には呼び出すようにします.

三角形をつくる

シーン中に,上図のような四角形ABCDがあったとします.Embreeでは四角形を扱うこともできますが,ここでは三角形ABDとBCDの2個があるものとみなしましょう.このシーンには三角形が2個と頂点が4つありますから,三角形ジオメトリを次のように作成します.
int ntri = 2; // シーン中の三角形の数
int nvert = 4; // シーン中の頂点の総数

/* 3角形ジオメトリを作成 */
unsigned geomID = rtcNewTriangleMesh2(scene, RTC_GEOMETRY_STATIC, ntri, nvert);

unsigned rtcNewTriangleMesh2 (RTCScene scene, RTCGeometryFlags flags, size_t numTriangles, size_t numVertices, size_t numTimeSteps = 1, unsigned int geomID = -1)
新しい三角形メッシュを作成し,そのジオメトリIDが戻り値になります.第1引数(scene)には既に作成してあるシーンを渡して下さい.第2引数(flags)にはジオメトリの種類を指定します.ジオメトリの種類はジオメトリ変更の頻度に応じてRTC_GEOMETRY_STATIC, RTC_GEOMETRY_DEFORMABLE, RTC_GEOMETRY_DYNAMICから選びますが,まずは動かないシーンを扱いますので今回はRTC_GEOMETRY_STATICを選択して下さい.第3引数(numTriangles)には作成する三角形の数を指定します.今は2個の三角形を登録します.第4引数(numVertices)には頂点の総数を指定します.今回作成する2個の三角形は合計4個の頂点から成り立っているので,4を指定します.第5引数(numTimeSteps)はメッシュがアニメーションする場合に使いますので,今回は指定しません.第6引数(geomID)はジオメトリのIDを明示的に与える場合に使うようですが,今回は指定しません.

次に,この三角形の頂点座標を記憶するための変数(vertex buffer:頂点バッファ)を次のように用意します.
/* 頂点座標をセット */
float* vertices = (float*)rtcMapBuffer(scene, geomID, RTC_VERTEX_BUFFER);
/* rtcMapBuffer関数とrtcUnmapBuffer関数に挟まれた間で頂点座標を指定します */
vertices[0] =  10; vertices[1]  =  10; vertices[2]  = 0; // 点Aの座標
vertices[3] =  10; vertices[4]  = -10; vertices[5]  = 0; // 点Bの座標
vertices[6] = -10; vertices[7]  =  10; vertices[8]  = 0; // 点Cの座標
vertices[9] = -10; vertices[10] = -10; vertices[11] = 0; // 点Dの座標
rtcUnmapBuffer(scene, geomID, RTC_VERTEX_BUFFER);
void* rtcMapBuffer(RTCScene scene, unsigned geomID, RTCBufferType type)
指定したタイプのバッファをマッピングし,先頭のアドレスを返します.第1引数(scene)には既に作成してあるシーンを,第2引数(geomID)には先程作成したジオメトリのIDを渡して下さい.第3引数(type)にはバッファのタイプを渡します.今回は頂点バッファを使いたいので,RTC_VERTEX_BUFFERとし,すぐ後で説明するインデックスバッファの指定にはRTC_INDEX_BUFFERを使います.
void rtcUnmapBuffer(RTCScene scene, unsigned geomID, RTCBufferType type)
rtcMapBuffer関数でマッピングしたバッファを解除します.後で説明するrtcCommit関数を使う前には必ずrtcMapBuffer関数を実行し,バッファの解除を忘れないようにして下さい.なお,関数の引数はrtcMapBufferと同一なので省略します.

次に,各三角形をなす頂点の接続情報(頂点の番号)を記録するための変数(index buffer:インデックスバッファ)を次のように用意します.

/* 頂点番号をセット */
/* rtcMapBuffer関数とrtcUnmapBuffer関数に挟まれた間で頂点番号を指定します */
int* indices = (int*)rtcMapBuffer(scene, geomID, RTC_INDEX_BUFFER);
indices[0] = 0; indices[1] = 1; indices[2] = 3; // 三角形ABD
indices[3] = 1; indices[4] = 2; indices[5] = 3; // 三角形BCD
rtcUnmapBuffer(scene, geomID, RTC_INDEX_BUFFER);

最後にこれまでに作成したジオメトリの情報をシーンに登録します.

/* シーンへ登録 */
rtcCommit(scene);

void rtcCommit (RTCScene scene)
バッファにセット済みのデータをシーンに登録します.ジオメトリの追加や修正を行ったらレイトレース前に呼び出して下さい.なお例によって第1引数(scene)には既に作成してあるシーンを渡して下さい.

レイとの交差判定

まず例として,上図のような始点Roが(0, 0, 20),方向が(0, 0, -1)のレイが三角形と交差するかどうかを判定しましょう.まずはこのようなレイを次のように作成します.
/* レイを生成する */
RTCRay rtcray;
/* レイの始点 */
rtcray.org[0] =  0.0;  // x
rtcray.org[1] =  0.0;  // y
rtcray.org[2] =  20.0; // z
/* レイの方向 */
rtcray.dir[0] =  0.0;  // x
rtcray.dir[1] =  0.0;  // y
rtcray.dir[2] = -1.0;  // z
/* 交差判定する範囲を指定 */
rtcray.tnear = 0.0f;     // 範囲の始点
rtcray.tfar = INFINITY;  // 範囲の終点.交差判定後には交差点までの距離が格納される.
Embreeでは,レイはRTCRay構造体で定義します.レイの始点,方向,交差判定する範囲(最大で[0, ∞])を指定し,後で説明する交差判定する関数に渡すと,交差点の情報などがRTCRay構造体のメンバに格納されて戻ってきます.RTCRay構造体のメンバでよく使いそうなものを下に書いておきます.
RTCRay構造体(よく使いそうなもの抜粋)
メンバ 入力・出力 説明
float org[3] 入力 レイの始点
float dir[3] 入力 レイの方向(正規化不要)
float tnear 入力 交差判定範囲の始点.0以上.
float tfar 入/出 交差判定範囲の終点.交差判定後には交差点までの距離が格納される.
float Ng[3] 出力 交差点の法線方向.正規化されていない.
unsigned geomID 出力 交差したジオメトリのID
unsigned primID 出力 交差したプリミティブのID
いよいよレイと三角形との交差判定をしていきます.
/* 交差判定 */
rtcIntersect(scene, rtcray);
if (rtcray.geomID == RTC_INVALID_GEOMETRY_ID)
{
	/* 交差点が見つからなかった場合 */
	std::cout << "Reject." << std::endl;
}
else
{
	/* 交差点が見つかった場合 */
	std::cout << "Intersect" << std::endl;
}
rtcIntersect関数に作成したシーンと交差判定したいレイを渡すと,渡したレイに交差判定結果が格納されて戻ってきます.具体的にはgeomIDメンバの値を見ればよく,RTC_INVALID_GEOMETRY_IDの場合は交差するジオメトリが見つからなかったことを示しています.上図のようなシーンが正しく作れていれば,コンソールにIntersectと表示されるはずです.

void rtcIntersect (RTCScene scene, RTCRay& ray)
第1引数(scene)には既に作成してあるシーンを,第2引数(ray)には交差判定したいレイを渡します.

以上をまとめたソースコードは次の通りです.

#include <iostream>
#include <embree2/rtcore.h>
#include <embree2/rtcore_ray.h>
int main(void)
{
	std::cout << "Hello Embree." << std::endl;

	/* デバイスを作成 */
	RTCDevice device = rtcNewDevice(NULL);

	/* シーンを作成 */
	RTCScene scene = rtcDeviceNewScene(device, RTC_SCENE_STATIC, RTC_INTERSECT1);

	int ntri = 2; // シーン中の三角形の数
	int nvert = 4; // シーン中の頂点の総数

	/* 3角形ジオメトリを作成 */
	unsigned geomID = rtcNewTriangleMesh2(scene, RTC_GEOMETRY_STATIC, ntri, nvert);

	/* 頂点座標をセット */
	float* vertices = (float*)rtcMapBuffer(scene, geomID, RTC_VERTEX_BUFFER);
	vertices[0] = 10; vertices[1] = 10; vertices[2] = 0; // 点Aの座標
	vertices[3] = 10; vertices[4] = -10; vertices[5] = 0; // 点Bの座標
	vertices[6] = -10; vertices[7] = 10; vertices[8] = 0; // 点Cの座標
	vertices[9] = -10; vertices[10] = -10; vertices[11] = 0; // 点Dの座標
	rtcUnmapBuffer(scene, geomID, RTC_VERTEX_BUFFER);

	/* 頂点番号をセット */
	int* indices = (int*)rtcMapBuffer(scene, geomID, RTC_INDEX_BUFFER);
	indices[0] = 0; indices[1] = 1; indices[2] = 3; // 三角形ABD
	indices[3] = 1; indices[4] = 2; indices[5] = 3; // 三角形BCD
	rtcUnmapBuffer(scene, geomID, RTC_INDEX_BUFFER);

	/* シーンへ登録 */
	rtcCommit(scene);

	/* レイを生成する */
	RTCRay rtcray;
	/* レイの始点 */
	rtcray.org[0] = 0; // x
	rtcray.org[1] = 0; // y
	rtcray.org[2] = 20; // z
	 /* レイの方向 */
	rtcray.dir[0] = 0; // x
	rtcray.dir[1] = 0; // y
	rtcray.dir[2] = -1; // z
	/* 交差判定する範囲を指定 */
	rtcray.tnear = 0.0f; // 範囲の始点
	rtcray.tfar = INFINITY;  // 範囲の終点.交差判定後には交差点までの距離が格納される.

	/* 交差判定 */
	rtcIntersect(scene, rtcray);
	if (rtcray.geomID == RTC_INVALID_GEOMETRY_ID)
	{
		/* 交差点が見つからなかった場合 */
		std::cout << "Reject." << std::endl;
	}
	else
	{
		/* 交差点が見つかった場合 */
		std::cout << "Intersect" << std::endl;
	}

	/* シーンを削除 */
	rtcDeleteScene(scene);

	/* デバイスを削除 */
	rtcDeleteDevice(device);

	return 0;
}

練習問題

  • 衝突位置までの距離はRTCRay構造体のtfarに格納されています.交差点の3次元位置座標を求めてコンソールに表示して下さい.図から,このシーンでは原点(0,0,0)で交差するものと予想されます.
  • 衝突した点の正規化済み法線ベクトルをコンソールに表示して下さい.図から,このシーンでの交差点における法線は(0, 0, 1)であると予想されます.

参考