Embree v3 入門

著者:久保 尋之
(公開:2019/12/184)

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

Embreeのインストール

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

作業用フォルダを準備

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

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

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

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

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

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

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

デバイスを作る

いよいよVisual Studio2017を使ってプログラムを作っていきます.Visual Studio2017を起動し,プロジェクトを新規作成する際に[Visual C++] - [Windows デスクトップ]から「Windwos デスクトップウィザード」を選び,「空のプロジェクト」を作成してください.次に,[ビルド] - [構成マネージャ]からアクティブプラットフォームを「x64」に指定して下さい.
さらに,[プロジェクト] - [プロパティ]からプロパティページを開き,[構成プロパティ] - [VC++ ディレクトリ]からインクルードディレクトリ

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

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

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

さらに,[リンカ-] - [入力] - [追加の依存ファイル]

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

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

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

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

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

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

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

RTCScene rtcNewScene(RTCDevice device)
新しいシーンを作成します.シーンにはTriangleなど様々なジオメトリを格納することができますが,その詳細については後述します.引数には先に作成したデバイスを入れます.

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

三角形をつくる

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

/* 3角形ジオメトリを作成 */
RTCGeometry geom = rtcNewGeometry(device, RTC_GEOMETRY_TYPE_TRIANGLE);

RTCGeometry rtcNewGeometry(RTCDevice device, enum RTCGeometryType type);
新しいジオメトリを作成します.第1引数(device)には既に作成してあるデバイスを渡して下さい.第2引数(type)にはジオメトリの種類を指定します.ジオメトリの種類は形状に応じて RTC_GEOMETRY_TYPE_TRIANGLE, RTC_GEOMETRY_TYPE_QUAD, RTC_GEOMETRY_TYPE_SUBDIVISIONなどから選びます.今回は三角形を扱うのでRTC_GEOMETRY_TYPE_TRIANGLEを指定します.

次に,この三角形の頂点座標を記憶するための変数(vertex buffer:頂点バッファ)を次のように用意します.
/* 頂点座標をセット */
float* vertices = (float*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 3 * sizeof(float), nvert);
vertices[0] = 10.f; vertices[1] = 10.f; vertices[2] = 0.f; // 点Aの座標
vertices[3] = 10.f; vertices[4] = -10.f; vertices[5] = 0.f; // 点Bの座標
vertices[6] = -10.f; vertices[7] = 10.f; vertices[8] = 0.f; // 点Cの座標
vertices[9] = -10.f; vertices[10] = -10.f; vertices[11] = 0.f; // 点Dの座標
void* rtcSetNewGeometryBuffer(
        RTCGeometry geometry,
        enum RTCBufferType type,
        unsigned int slot,
        enum RTCFormat format,
        size_t byteStride,
        size_t itemCount
        );
指定したタイプのジオメトリバッファを作成し,先頭のアドレスを返します.第1引数(geometry)には既に作成してあるジオメトリを渡し,第2引数(type)には作成するジオメトリバッファの種類を指定します.今回は頂点バッファを作成したいので,RTC_BUFFER_TYPE_VERTEXを指定して下さい.第3引数(slot)はひとまず0で良い様ですが,詳細はよくわかりません. 第4引数(format)はデータのフォーマットを指定します.今回は1つの頂点あたり,単精度浮動小数点(float)が3つ(3次元だから)並んだデータにするので,RTC_FORMAT_FLOAT3を指定します. 第5引数(byteStride)は1つの頂点あたりのデータサイズを指定します.今回はfloatが3つ並んでいるので,サイズは3*sizeof(float)となります. 最後に,第6引数(itemCount)には要素の数を指定します.今回は頂点数に相当するnvertを指定します.

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

/* 頂点番号をセット */
unsigned* indices = (unsigned*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(unsigned), ntri);
indices[0] = 0; indices[1] = 1; indices[2] = 3; // 三角形ABD
indices[3] = 1; indices[4] = 2; indices[5] = 3; // 三角形BCD

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

/* シーンへ登録 */
rtcCommitGeometry(geom);
rtcAttachGeometry(scene, geom);
rtcReleaseGeometry(geom);
rtcCommitScene(scene);

void rtcCommitGeometry(RTCGeometry geometry)
ジオメトリにセット済みのデータをシーンに登録します.ジオメトリの追加や修正を行ったらレイトレース前に呼び出して下さい.

レイとの交差判定

まず例として,上図のような始点Roが(0, 0, 20),方向が(0, 0, -1)のレイが三角形と交差するかどうかを判定しましょう.まずはこのようなレイを次のように作成します.
/* レイを生成する */
struct RTCRayHit rayhit;

/* レイの始点 */
rayhit.ray.org_x = 0.0;  // x
rayhit.ray.org_y = 0.0;  // y
rayhit.ray.org_z = 20.0; // z
/* レイの方向 */
rayhit.ray.dir_x = 0.0;  // x
rayhit.ray.dir_y = 0.0;  // y
rayhit.ray.dir_z = -1.0;  // z
/* 交差判定する範囲を指定 */
rayhit.ray.tnear = 0.0f;     // 範囲の始点
rayhit.ray.tfar = INFINITY;  // 範囲の終点.交差判定後には交差点までの距離が格納される.
(説明は後で書きます...)

いよいよレイと三角形との交差判定をしていきます.

struct RTCIntersectContext context;
rtcInitIntersectContext(&context);

/* 交差判定 */
rtcIntersect1(scene, &context, &rayhit);

if (rayhit.hit.geomID == RTC_INVALID_GEOMETRY_ID)
{
    /* 交差点が見つからなかった場合 */
    std::cout << "Reject." << std::endl;
}
else
{
    /* 交差点が見つかった場合 */
    std::cout << "Intersect" << std::endl;
}
rtcIntersect1関数に作成したシーンと交差判定したいレイを渡すと,渡したレイに交差判定結果が格納されて戻ってきます.具体的にはgeomIDメンバの値を見ればよく,RTC_INVALID_GEOMETRY_IDの場合は交差するジオメトリが見つからなかったことを示しています.上図のようなシーンが正しく作れていれば,コンソールにIntersectと表示されるはずです.

void rtcIntersect1(
        RTCScene scene,
        struct RTCIntersectContext* context,
        struct RTCRayHit* rayhit
        )
説明は後で書きます.

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

#include <iostream>
#include <embree3/rtcore.h>
#include <embree3/rtcore_ray.h>
int main(void)
{
	/* デバイスを作成 */
	RTCDevice device = rtcNewDevice(NULL);
	/* シーンを作成 */
	RTCScene scene = rtcNewScene(device);

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

	/* 3角形ジオメトリを作成 */
	RTCGeometry geom = rtcNewGeometry(device, RTC_GEOMETRY_TYPE_TRIANGLE);

	/* 頂点座標をセット */
	float* vertices = (float*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_VERTEX, 0, RTC_FORMAT_FLOAT3, 3 * sizeof(float), nvert);
	vertices[0] = 10.f; vertices[1] = 10.f; vertices[2] = 0.f; // 点Aの座標
	vertices[3] = 10.f; vertices[4] = -10.f; vertices[5] = 0.f; // 点Bの座標
	vertices[6] = -10.f; vertices[7] = -10.f; vertices[8] = 0.f; // 点Cの座標
	vertices[9] = -10.f; vertices[10] = 10.f; vertices[11] = 0.f; // 点Dの座標

	/* 頂点番号をセット */
	unsigned* indices = (unsigned*)rtcSetNewGeometryBuffer(geom, RTC_BUFFER_TYPE_INDEX, 0, RTC_FORMAT_UINT3, 3 * sizeof(unsigned), ntri);
	indices[0] = 0; indices[1] = 1; indices[2] = 3; // 三角形ABD
	indices[3] = 1; indices[4] = 2; indices[5] = 3; // 三角形BCD

	/* シーンへ登録 */
	rtcCommitGeometry(geom);
	rtcAttachGeometry(scene, geom);
	rtcReleaseGeometry(geom);
	rtcCommitScene(scene);


	/* レイを生成する */
	struct RTCRayHit rayhit;

	/* レイの始点 */
	rayhit.ray.org_x = 0.0;  // x
	rayhit.ray.org_y = 0.0;  // y
	rayhit.ray.org_z = 20.0; // z
	/* レイの方向 */
	rayhit.ray.dir_x = 0.0;  // x
	rayhit.ray.dir_y = 0.0;  // y
	rayhit.ray.dir_z = -1.0;  // z
	/* 交差判定する範囲を指定 */
	rayhit.ray.tnear = 0.0f;     // 範囲の始点
	rayhit.ray.tfar = INFINITY;  // 範囲の終点.交差判定後には交差点までの距離が格納される.

	/* 交差判定 */
	struct RTCIntersectContext context;
	rtcInitIntersectContext(&context);

	/* 交差判定 */
	rtcIntersect1(scene, &context, &rayhit);

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

	/* シーンを削除 */
	rtcReleaseScene(scene);
	/* デバイスを削除 */
	rtcReleaseDevice(device);
	return 0;
}

練習問題

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

参考