Skip to content
shocker-0x15 edited this page Jan 13, 2019 · 17 revisions

VLR実装解説

OptiXの簡単なおさらい

まず大元としてoptix::Contextがあり、名前の通りOptiXのコンテキストを管理する。後述のOptiXオブジェクトはすべてoptix::Contextを経由して生成される。

物体の定義

シーン中の各物体の見た目をコントロールするためのOptiXオブジェクトとしては次のものが挙げられる。

  • optix::Geometry
    Intersection / Bounding Box Program (後述) (とそこから読まれる頂点バッファーやインデックスバッファー)を関連付けることで物体の形状を定義する。
  • optix::Material
    Closest Hit / Any Hit Program (後述)を関連付ける。
  • optix::GeometryInstance
    ひとつのoptix::Geometryと複数のoptix::Materialを関連付ける。

プログラム

OptiXではユーザーがいくつかの種類の(optix::ProgramというOptiXオブジェクトで管理する)プログラムを記述することでレンダラーを作り上げる。

  • Ray Generation
    レイの生成を行う。ここからrtTrace()を呼ぶことで後述のClosest Hit Programなどが起動されていくためエントリーポイントとして考えられる。optix::Contextには複数のエントリーポイントを作ることができる。
  • Exception
    プログラム実行中に例外が起きた場合は処理が中断されてException Programが呼ばれる。Ray Generation Programとともにエントリーポイントごとに設定できる。
  • Closest Hit
    レイを飛ばしたときに最も近くにある衝突点で起動される。シェーディングは主にここに記述することになる。
    rtTrace()をここから再帰的に呼ぶことができる。 OptiXではレイの種類を任意の数設定できて、レイの種類ごとに設定する。
  • Any Hit
    Closest Hit Programが起動されるまでの過程において、レイが物体と交差するたび何度も起動される。アルファテストなどはここで行う。レイの種類ごとに設定する。
  • Miss
    レイトレース時になんの物体にもヒットしなかった場合に起動される。レイの種類ごとに設定する。
  • Intersection
    レイと物体の衝突判定を記述する。プリミティブインデックスが取得できるため、例えば三角形メッシュでは処理すべき三角形を特定できる。
    任意の構造体をattributeと呼ばれるシステム変数としてClosest Hit / Any Hit Programへと伝えることができる。
  • Bounding Box
    物体を構成するプリミティブごとのBounding Box (AABB)を計算する。
  • Selector Visit
    後述のシーングラフをトラバース中に動的にメッシュのLOD選択などを行う際に記述する。
  • Callable
    任意のプログラム中から呼ぶことのできるプログラムで目的も任意。実質関数ポインター。

シーングラフ

前述のoptix::GeometryInstanceをまとめあげてシーングラフを構成できる。

  • optix::GeometryGroup
    お互いの相対的な位置関係が不変と考えられるGeometryInstanceを複数個登録する。OptiXはGeometryGroupに対してレイと物体との衝突判定を高速化するための加速機構(BVHなど)を計算する。
  • optix::Group
    GeometryGroup, Group, Transform, Selectorを子として複数個登録する。OptiXはGroupに対しても加速機構を計算する。
  • optix::Transform
    GeometryGroup, Group, Transform, Selectorを子としてひとつ登録する。シーン中の物体の移動・回転はTransformの値の変化を通じて実現される。
    Transformを変化させた場合には上位の階層にあるGroupの加速機構の再計算をOptiXに対して指示する必要がある。
  • optix::Selector
    GeometryGroup, Group, Transform, Selectorを子として複数個登録する。前述のSelector Visit Programと合わせて物体のLOD選択を実現する。

変数・バッファーのバインドとスコープ

プログラム実行中に参照するバッファーやテクスチャー、変数をプログラム自身やシーングラフを構成するOptiXオブジェクトにバインドすることできる。またプログラム中の同じ変数やバッファーに対して複数のスコープでバインドを行うことできる。参照の優先順位は以下のようになっている。

Program 1 2 3 4
Closest Hit / Any Hit Program GeometryInstance Material Context
Intersection / Bounding Box Program GeometryInstance Geometry Context
Ray Generation / Exception / Miss Program Context
Selector Visit Program Node

Callable ProgramはそれがバインドされているProgramのスコープを継承する。 後述のBindless Callable Programの場合はCallable Program自身が一番、二番目にContextを参照する。

光輸送アルゴリズムの実装

VLRではパストレーシングを光輸送アルゴリズムとして実装している。
=> Ray Generationでプライマリレイを生成してrtTrace()を呼んだ後、Closest Hitから再帰的にrtTrace()を行うことでパスを構築する。

レイトレーシングと言えば概念的には再帰的なアルゴリズムではあるが、パストレーシングはただの末尾再帰なのでスタック用の変数も用意することなくただのwhileループで記述することできる。再帰呼び出しはGPUのレジスター消費もかなり大きくなるし、再帰呼び出し回数にも制限がある。whileループにすることでこれらの問題は解決する。

// Ray Generation Program

// ピクセルごとの初期化処理
...

while (true) {
    payload.terminate = true;
    ++pathLength;
    if (pathLength >= MaxPathLength)
        payload.maxLengthTerminate = true;
    rtTrace(pv_topGroup, ray, payload); // パスが延長される場合はpayload.terminate == false

    if (payload.terminate)
        break;
    VLRAssert(pathLength < MaxPathLength, "Path should be terminated... Something went wrong...");

    ray = optix::make_Ray(asOptiXType(payload.origin), asOptiXType(payload.direction), RayType::Scattered, 1e-4f, FLT_MAX);
}

// サンプルした寄与の蓄積
...

パストレーシングのアルゴリズムについては以下を参照。
https://rayspace.xyz/CG/contents/path_tracing/
https://speakerdeck.com/shocker_0x15/path-tracing

attribute変数の最小化

衝突点の情報をIntersection Programで全て計算してattributeとして保持するのは、衝突点が実際に最近点になるか不明かつGPUのレジスター使用量にも悪影響があるため効率が悪い。物体表面の位置はプリミティブインデックスと2つの変数があれば特定できる。Closest Hit Programからこれらの情報を用いて最も近い衝突点の情報を復元する。

struct HitPointParameter {
    float b0, b1; // 三角形の場合は重心座標(barycentric coordinates)
    int32_t primIndex;
};

Closest Hit ProgramからはGeometryスコープの変数にはアクセスできない。頂点バッファーなどの幾何情報にアクセスする必要があるため、それらの情報はGeometryではなく、GeometryInstanceスコープにバインドしておく(Intersection / Boudning Box ProgramからGeometryInstanceは参照することができる)。

様々なマテリアルの実現

OptiXではoptix::MaterialごとにClosest Hit / Any Hit Programを設定できる。
=> マテリアル種別ごとに異なるClosest Hit / Any Hit Programを用意する。

実際にパストレーシングのロジックを考えてみると、マテリアル種別によって差が生じる場所はBSDFの評価やBSDFサンプリング部分だけで、implicit light path, NEE, RR, ウェイトの計算など共通する部分が非常に多い。せっかくの共通する部分を異なるClosest Hit Programで実行するとdivergenceだらけになってGPUのSIMD稼働率が下がる。
=> マテリアル種別によって異なる部分をCallable Programで処理する。Closest Hit / Any Hitはそれぞれ一種類、全体で共通にする。

OptiX 3.6からの仕様に含まれているBindless Callable Programの仕組みを使えば、optix::MaterialごとにCallable Programをいくつも直接バインドするのではなく、Callable Program(のID)をマテリアル種別ごとにまとめた構造体(BSDF/EDFProcedureSet)を記録したバッファーをContextスコープで用意(pv_bsdf/edfProcedureSetBuffer)、各optix::Materialにはそのバッファー中のインデックスをバインドするだけで済む。パフォーマンス面は不明だが、柔軟性はこの方法のほうが高い。
さらに言えば、optix::GeometryInstanceごとにoptix::Materialはひとつという運用にすることで、GeometryInstanceスコープに前述のインデックスをバインドしてマテリアルを区別させることもできる。この場合optix::Materialの役目は、共通のClosest Hit / Any Hit Programを関連付けるだけになり全体でひとつあれば十分になる。

マテリアルの種類ごとにBSDF/EDFのパラメター構築のために読む変数やテクスチャー・バッファー、そしてそれらの数は異なる。 これらはBindless Texture / Bufferの仕組みを使ってCallable Programと同じように、マテリアルごとに読むテクスチャー(のID)やパラメターをまとめた汎用的な構造体(SurfaceMaterialDescriptor)を記録したバッファーをContextスコープで用意(pv_materialDescriptorBuffer)、前述のインデックスを用いて適切な情報を読む。構築されるBSDF/EDFのパラメターを格納した構造体もまたマテリアルごとに異なる形をとる必要があるため、これらも汎用的な構造体を必要とする。

// 「マテリアル種別」ごとにユニークな、反射・透過処理に関するCallableProgramのIDをまとめた構造体
struct BSDFProcedureSet {
    int32_t progGetBaseColor;
    int32_t progMatches;
    int32_t progSampleInternal;
    int32_t progEvaluateInternal;
    int32_t progEvaluatePDFInternal;
    int32_t progWeightInternal;
};

// 「マテリアル種別」ごとにユニークな、発光処理に関するCallableProgramIDをまとめた構造体
struct EDFProcedureSet {
    int32_t progEvaluateEmittanceInternal;
    int32_t progEvaluateInternal;
};

// 「マテリアル」ごと (!= マテリアル種別ごと)のデータ
// マテリアル種別に対応する処理のCallableProgramIDをまとめた構造体のインデックス(bsdf/edfProcedureSetIndex)、
// マテリアルごとのデータ(data)
// マテリアル種別に対応するセットアップ処理のCallableProgramID(progSetupBSDF/EDF)
// を持つ。
struct SurfaceMaterialDescriptor {
    int32_t progSetupBSDF;
    uint32_t bsdfProcedureSetIndex;
    int32_t progSetupEDF;
    uint32_t edfProcedureSetIndex;
#define VLR_MAX_NUM_MATERIAL_DESCRIPTOR_SLOTS (28)
    uint32_t data[VLR_MAX_NUM_MATERIAL_DESCRIPTOR_SLOTS];
};

...

rtBuffer<BSDFProcedureSet, 1> pv_bsdfProcedureSetBuffer;
rtBuffer<EDFProcedureSet, 1> pv_edfProcedureSetBuffer;
rtBuffer<SurfaceMaterialDescriptor, 1> pv_materialDescriptorBuffer;

まとめると、
Closest Hit Programでマテリアルインデックス取得
=> 汎用的なマテリアルデスクリプター構造体SurfaceMaterialDescriptorと読み解き方を表現したCallable Program progSetupBSDF/EDFを読む。 => 読み解き方を表現したCallable ProgramでBSDF/EDFパラメターdataを読んで、汎用的なBSDF/EDFパラメター構造体(BSDF/EDF、上記には無い)をセットアップする。 => BSDF/EDFごとのCallable Programによって汎用的なBSDF/EDFパラメター構造体を適切に処理する。

アルファテスト

アルファテクスチャーによる物体形状への影響を実現するには、Any Hit Programでアルファテクスチャーをサンプル、値に応じて(確率的に)衝突を無視すれば良い。 シーン中のアルファテストを必要とする物体の割合にもよるかもしれないが、全ての物体でアルファテストを行わせるのは効率が悪い可能性がある。そのため、Any Hit Programはアルファテストの有無で使い分ける場合があり、全体で2つのoptix::Materialを用意した。

シェーダーノード

各マテリアルはパストレーシングの反復処理の最中、衝突点においてBSDFを構築する。BSDFのパラメターは即値で与えることもできるが、マテリアルは各パラメターに対応するノード入力ソケットを持っており、型の一致する任意のノードを接続することで、実行時に衝突点の持つ属性に応じて柔軟に設定できる。BSDFのパラメターの取得元としてごく一般的な画像テクスチャーのサンプリングもこのノードシステムを用いて実現する。また、ノード自身もそのパラメターとしてノード入力ソケットを持っていることがあり、ノードを多段に接続することもできる。

// マテリアル情報
// SurfaceMaterialDescriptorのdata部に格納されている。
struct MatteSurfaceMaterial {
    ShaderNodeSocketID nodeAlbedo; // アルベドノード
    TripletSpectrum immAlbedo; // アルベドの即値
};

...

// 構築されるBSDFパラメター
struct MatteBRDF {
    SampledSpectrum albedo;
};

参考文献

Implement Physically Based Ray Tracing with OptiX and MDL