これはレイトレ合宿7のアドベントカレンダーの記事です。
容赦無い数式が名物とも言えるレイトレ合宿アドベントカレンダーですが、レイトレ合宿の面白いところには人様の書いたレイトレーサーの実装を垣間見られるというものもあります。
そこで今回はMetal Performance Shaderのレイトレーシングの話です。 Appleが絶賛実装中のGPUレイトレーサーをささっと見てみようと思います。
Metal Performance Shader
ところでMetalがAppleのグラフィックスAPIなのは知っているけどPerfomance Shaderとは、という方もいると思うので軽く解説します。
超ざっくり言うとApple謹製のGPGPU関連のボイラープレートコード集です。
御多分に洩れずMetalでも一発コンピュートシェーダーを走らせたいだけでもそこそこな量の前準備が必要になります。 しかもだいたい同じようなことを何度も書くためにコピペして、変えなくてはいけない変数や数値を変え忘れて時間を無駄にしたりするわけです。
そんなことをしているとこの辺誰か詳しい人がいい感じにまとめてくれると嬉しいのに、なんて思ったりします。 そこでなんと掛け値無しに一番詳しいAppleさんがまとめてくれたのがMetal Performance Shaderです。
以下面倒臭いのでMPSと略します。
MPSのレイトレーシングはNVIDIA RTXとDirectXのDXRが電撃的に発表された直後のWWDC2018で追加されました。 Metal Performance ShaderにあるRay Tracingという項目がそれです。 とはいえAppleも対抗したかったのか急いで出してきたようで、いまだにwebのドキュメントはほとんど空白で詳しい解説はヘッダーに書いてある有様です。
できること
MPSのレイトレーシングでできることは三角メッシュで構築されたシーンとレイとの交差判定です。インスタンシングもできます。
レイを詰め込んだバッファーを入れると結果を入れたいバッファが交差判定の結果で埋まるという塩梅です。 それをGPUでやってくれます。
交差判定の結果をどう料理するかはこちらで好きなようにMetalのコンピュートシェーダーを書くことになります。 反射するレイを生成するとか、ライトをサンプリングするとか、フォトンを探索するとかご自由に、という感じです。
Metalで実装されたEmbreeに近いものと言えるかもしれません。
Ray Tracing
さて、ではどのようなものが用意されているかといえばmacOS Mojaveでは以下のようなものです。
- MPSRayIntersector
- MPSAccelerationStructureGroup
- MPSAccelerationStructure
- MPSTriangleAccelerationStructure
- MPSInstanceAccelerationStructure
たった5つのクラス、しかもMPSAccelerationStructure
はベースクラスで、MPSAccelerationStructureGroup
はインスタンシング用のグループを定義するクラスです。
つまり実質3つのクラスしかありません。
思いの外こじんまりとした印象かもしれませんが、それぞれの用途はクラス名を見ればなんとなく想像できると思います。
AccelerationStructure
- MPSTriangleAccelerationStructure
- MPSInstanceAccelerationStructure
というわけでまずはこの2つ。 その名の通り三角形とインスタンスのAccelerationStructureです。
一番簡単な使い方はMPSTriangleAccelerationStructure
だけを使うものでしょう。
最近まで唯一のサンプルコードだったMetal for Accelerating Ray Tracingもそのような使い方のサンプルです。
accelerationStructure = [[MPSTriangleAccelerationStructure alloc] initWithDevice:device]; accelerationStructure.vertexBuffer = vertexBuffer; accelerationStructure.triangleCount = triangleCount; // 頂点インデックスがあるなら accelerationStructure.indexBuffer = indexBuffer; // トライアングルのマスクを使うなら accelerationStructure.maskBuffer = maskBuffer; [accelerationStructure rebuild];
こんな感じになるでしょうか。
もちろんそれぞれのバッファのoffset
やstride
といったお馴染みのプロパティもありますのでお好みなデータ構造を使えます。
馴染みが無いのはmaskBuffer
です。
これは各三角形に割り当てたビットフラグです。したがって長さはtriangleCount
と同じになります。
後述のレイの構造体にもmask
というフィールドが存在するものがあり、この値とANDをとって0でなければ交差判定をする、というものだそうです。
#define TRIANGLE_MASK_GEOMETRY 1 #define TRIANGLE_MASK_LIGHT 2 #define RAY_MASK_PRIMARY 3 #define RAY_MASK_SHADOW 1 #define RAY_MASK_SECONDARY 1
上記のサンプルの ShaderTypes.h にはこんな定義があります。 このサンプルコードは光源を明示的にサンプルしていく実装になっているので、最初のカメラからのレイ以外は光源を無視するようにマスクを設定しています。
MPSInstanceAccelerationStructure
を使う場合にはMPSAccelerationStructureGroup
でAccelerationStructureをグループ化します。
// グループの用意 group = [[MPSAccelerationStructureGroup alloc] initWithDevice:device]; // InstanceAccelerationStructureで扱うTriangleAccelerationStructureは1つのvertexBufferを共有していなければならない totalVertexCount = 0; for (SceneObject *object in sceneObjects) totalVertexCount += object.vertexCount; vertexBuffer = [_device newBufferWithLength:totalVertexCount * sizeof(Vertex) options:options]; // vertex以外のバッファもあれば同様に ... // まずはインスタンスの元になるオブジェクトのMPSTriangleAccelerationStructureをつくる vertexOffset = 0; for (SceneObject *object in sceneObjects) { accelerationStructure = [[MPSTriangleAccelerationStructure alloc] initWithGroup:group]; // bufferのoffsetでどうにかする accelerationStructure.vertexBuffer = vertexBuffer; accelerationStructure.vertexBufferOffset = vertexOffset * sizeof(Vertex); accelerationStructure.triangleCount = object.triangleCount; ... vertexOffset += object.vertexCount; [accelerationStructure rebuild]; [triangleAccelerationStructures addObject:accelerationStructure]; } // MPSInstanceAccelerationStructureをつくる instanceAccelerationStructure = [[MPSInstanceAccelerationStructure alloc] initWithGroup:group]; // インスタンス元のMPSTriangleAccelerationStructureのarray instanceAccelerationStructure.accelerationStructures = triangleAccelerationStructures; // 置きたいMPSTriangleAccelerationStructureのインデックスを並べたバッファ instanceAccelerationStructure.instanceBuffer = instanceBuffer; instanceAccelerationStructure.instanceCount = numInstances; // インスタンスの位置を指定したければ instanceAccelerationStructure.transformType = MPSTransformTypeFloat4x4; instanceAccelerationStructure.transformBuffer = instanceTransformBuffer; // マスクを使いたければ instanceAccelerationStructure.maskBuffer = instanceMaskBuffer; [instanceAccelerationStructure rebuild]
instanceBuffer
はシーン中にインスタンスとして置きたいMPSTriangleAccelerationStructure
のインデックスです。
transformBuffer
と対応した位置に置かれます。
transformType
はMPSTransformTypeIdentity
とMPSTransformTypeFloat4x4
の2種類で、単位行列しか使わない場合はトランスフォームの処理をスキップするためのもののようです。
ちなみにAccelerationStructure
はrebuild
をしないと使えません。いくつか構築する時のオプションなどがあります。
// MPSAccelerationStructureのusageプロパティで使用するenum MPSAccelerationStructureUsageNone // 特に指定なし MPSAccelerationStructureUsageRefit // 変化があっても基本的にリフィットで対応する MPSAccelerationStructureUsageFrequentRebuild // 頻繁にリビルドしたい
まずusage
に設定するenumがあります。
これはAccelerationStructureの構築アルゴリズムを決めるためのもので、
MPSAccelerationStructureUsageRefit
は構築に時間がかかるがレイの交差判定が高速になるアルゴリズムを使い、MPSAccelerationStructureUsageFrequentRebuild
では構築は早いものの交差判定のパフォーマンスが落ちるアルゴリズムが使われるらしいです。
なお無指定のMPSAccelerationStructureUsageNone
の扱いは不明です。
// その場で構築 - (void)rebuild; // 並列で構築して終わったらcompletionHandlerを呼び出す - (void)rebuildWithCompletionHandler:(nonnull MPSAccelerationStructureCompletionHandler)completionHandler; // リフィットコマンドをコマンドバッファーに積む - (void)encodeRefitToCommandBuffer:(nonnull id <MTLCommandBuffer>)commandBuffer;
それに対応してリビルドとリフィットのメソッドがあります。
見てわかるようにリフィットはGPUで実行されます。
もちろんリフィットは一度rebuild
しないと使えません。
これを利用して並列で再構築している最中はリフィットして、リビルドが終わったら構築済みのものと差し替える、というAccelerationStructureのダブルバッファリングが MPSAccelerationStructure.h のコメント内で提案されていたりします。
それからMPSAccelerationStructure
はNSSecureCoding
プロトコルを実装しているのでNSKeyedArchiver
でシリアライズが可能です。
普通にplistが出てきます。中を見るとBVHなんとかみたいなものが見えるのでBVHの一種のようです。
静的なシーンはMPSAccelerationStructureUsageRefit
で構築して保存して使うと良いと思うぜ 、とコメントの人は書いています。
MPSRayIntersector
次はMPSRayIntersector
です。
文字通りレイとMPSAccelerationStructure
との交差判定をとります。
// MPSRayIntersectorの作成 intersector = [[MPSRayIntersector alloc] initWithDevice:device]; // レイのデータ構造のタイプ intersector.rayDataType = MPSRayDataTypeOriginMaskDirectionMaxDistance; intersector.rayStride = rayStride; // 使いたいマスクのフラグ intersector.rayMaskOptions = MPSRayMaskOptionPrimitive; // レイを詰め込むバッファーを用意 rayBuffer = [device newBufferWithLength:sizeof(Ray) * rayCount options:options]; // 交差判定の結果が入るバッファーを用意 intersectionBuffer = [device newBufferWithLength:sizeof(Intersection) * rayCount options:0]; id <MTLCommandBuffer> commandBuffer = [commandQueue commandBuffer]; // 交差判定の結果の構造体のデータタイプ intersector.intersectionDataType = MPSIntersectionDataTypeDistancePrimitiveIndexCoordinates; // レイトレース [intersector encodeIntersectionToCommandBuffer:commandBuffer intersectionType:MPSIntersectionTypeNearest rayBuffer:rayBuffer rayBufferOffset:0 intersectionBuffer:intersectionBuffer intersectionBufferOffset:0 rayCount:rayCount accelerationStructure:accelerationStructure]; [commandBuffer commit];
基本的な流れはこんな感じです。
rayDataType
とintersectionDataType
はいくつか用意されているレイと判定結果の構造体のタイプを指定します。
// MPSRayDataType MPSRayDataTypeOriginDirection MPSRayDataTypeOriginMinDistanceDirectionMaxDistance MPSRayDataTypeOriginMaskDirectionMaxDistance // MPSIntersectionDataType MPSIntersectionDataTypeDistance MPSIntersectionDataTypeDistancePrimitiveIndex MPSIntersectionDataTypeDistancePrimitiveIndexCoordinates MPSIntersectionDataTypeDistancePrimitiveIndexInstanceIndex MPSIntersectionDataTypeDistancePrimitiveIndexInstanceIndexCoordinates
それぞれはこんな えらく長い名前の enumが定義されていて、それぞれの説明には例えばMPSRayDataTypeOriginDirection
には Use the MPSRayOriginDirection struct type というような説明になっていない説明のコメントがついています。
実はそれらの構造体の定義は MPSRayIntersectorTypes.h にあって、 MetalPerformanceShaders/MetalPerformanceShaders.h をincludeすることでMetalのシェーダーと共用で使えるようになっています。
交差判定はコマンドバッファーにコマンドを積んで、rayBuffer
に詰めたレイの判定結果が実行後に対応する位置のintersectionBuffer
に入ります。
前述の通りあくまで交差判定の結果なので、そこから反射するレイを生成したり、光源へシャドウレイを飛ばしたりといった処理をするには自分でコンピュートシェーダーを用意する必要があります。
とはいえここから先は
intersectionBuffer
の内容からrayBuffer
を新しいレイで埋めるシェーダーをエンコード[intersecter encodeIntersectionToCommandBuffer:...]
という流れを繰り返すだけで、GPU上でレイトレーシングをすることができます。
実際の流れは上にも挙げたMetal for Accelerating Ray Tracingを見るのが手っ取り早いと思います。
意外と面倒くさいです。
ちなみにMPSRayIntersector
のヘッダーにはかなり長い説明のコメントが書いてあります。
基本的な使い方からパフォーマンスに関するアドバイス的なことまであり、いくつかピックアップしてみると、
- レイや交差判定の構造体は小さいタイプを選んだ方がパフォーマンスが良い
- インデックスバッファーやマスクバッファーは使わない方がパフォーマンスが良い
- メモリが許すなら全部三角形に展開してしまった方がパフォーマンスが良い
- わざわざray bufferを縮める処理をするより不正なレイ(たとえば最大距離を負の値にしたり方向ベクトルをゼロにする)を突っ込んだ方がパフォーマンスが良い
などというまあそうでしょうけどみたいなことが書いてあって面白いです。
これから
6月にWWDC2019がありました。
次のバージョンのmacOSとiOSが発表されて、 意外にも MPSのレイトレーシングにも少し改良が予定されています。
MPSAccelerationStructure
がGPUでrebuild
できるようになるMPSPolygonAccelerationStructure
が追加されMPSTriangleAccelerationStructure
はそのサブクラスとなる。またMPSQuadrilateralAccelerationStructure
が追加される。rayBuffer
の代わりにrayTexture
が使えるメソッドが追加されるrayIndexBuffer
というバッファを使った交差判定メソッドが追加される
ざっと見て気になったものを挙げてみるとこんなところでしょうか。
1は文字通りで、コマンドバッファにrebuild
相当のものを積むメソッドが追加されます。
ダブルバッファリングしなくて済むくらい高速なのでしょうか。
2は三角形以外も扱えるようにしたいということでしょう。早速四辺形が追加されています。SceneKitはopenSubDivのテッセレーションを使っているのを売りにしているので、ゆくゆくはCatmul-Clark細分割曲面をレイトレースしたいのかもしれません。
3はラスタライザとのハイブリッドレンダリングをしやすくする改良のようです。新しいサンプルコードAnimating and Denoising a Raytraced Sceneがまさにそのハイブリッドレンダリングのサンプルで、ラスタライザのマルチレンダーターゲットを使ってレイテクスチャを作ってMPSのレイトレーシングに使うというものになっています。
4は 今までrayBufferを縮めるより不正なレイを入れた方が良いと言っていたが我々はより良いソリューションを見つけ出した というやつです。インデックスだけ詰め直して縮めれば良いんじゃないということらしいです。
それからどこに分類されるのかよくわかりませんが、動画の前のフレームの情報を使うタイプのフィルタが追加されています。
- MPSTemporalAA
- MPSSVGF
TemporalAAは最近のゲームではお馴染みのアンチエイリアスフィルタでしょう。
SVGFはSpatiotemporal Variance- Guided Filteringのことで、デモ動画は衝撃的でした。
そしてこれを使うMPSSVGFDenoiser
というデノイザーのクラスも追加されています。
RTXなんてなくてもリアルタイムGPUレイトレできらぁと言わんばかりの環境がmacOSとiOSには整いつつあるようです。
次のバージョンのOSのリリースが楽しみになりますね。