nVidiaのDXRチュートリアルやってみた

久々に技術ブログ書こうと思い、nVidiaのDXRチュートリアルをやってみました。

developer.nvidia.com

はじめに

チュートリアルは英語ですが、chromeの自動翻訳に投げればかなりいい感じの日本語になるので臆せず進みましょう。

 

こういうチュートリアルやるときには、意識すべきことがあって、それは「きちんと解説読みながら、できるだけ理解しながら、できるだけ素早くコーディングすること」かなと思っています。

①できるだけ理解しながら、ってのは当然のことで、理解しようと試みずにチュートリアルをやるとただただコードを書き写しただけ、いわゆる「写経」になってしまいますね。それは避けましょう。でもどうしても分からない部分は理解を後回しにしてもいいです。

②素早くコーディングってのは、あんまし時間をかけちゃうと興味も薄れるし、せっかく手を動かして身につけようとしてるのに、知識が断片化して効果半減だからです。しっかり理解をしようとするのは良いのですが、あまり時間をかけるのも良くないと思っています。

 

チュートリアルに取り掛かる前に・・・

チュートリアルに取り掛かる前に、可能な限り初歩的なDirectX12および初歩的なレイトレーシングの仕組みは取り掛かる前に理解しておいた方がいいと思いました。また、特有の用語もバンバン出て来るため、それを把握したうえでないと「何言ってんのこいつ」となるでしょう。

一応ぼくがチュートリアルやるうえで「これ最初に知らんとあかんやろ」と思った用語を挙げておきます。

加速構造(Acceleration Structure)

はい、なんなんすかね、これ。細かく言うとこいつにもTLAS(Top Level Acceleration Stucture)とBLAS(Bottom Level Acceleration Stucture)があるんですが、それはさておき。AccelerationStructureってなんだよ!!って思ったので、検索するとやっぱりレイトレーシングに関する用語のようで、

www.scratchapixel.com

なんてリンクがでてくるのですが、BVH(Bounding Volume Hierarchy)とか出てきます。大雑把に言うと、レイトレの効率を上げるために、バウンディングボックスやバウンディングスフィアなどのツリーを用いましょうって事みたい。

それの事をAccelerationStructureって言ってるようだ。DXRじたいの解説を見てもBLASーTLASの所でツリー構造になってるし、多分これの事だろうと思って先に進むことにする。

ペイロード(payload)

なにこれ・・・?一応調べると「正味のデータ部分」とか出てきます。わからんて!!

まぁ、どうやら、ヘッダなどを取り除いたデータ部分の事のようです。この程度の認識でも先に進むと何となくわかってくるのですが、大雑把に言うと、レイトレに関連するシェーダ同士のデータの受け渡しに使うものと思ってもらえればいいんじゃないかなと思います。

 

だいたいDXR特有の用語はこれくらいかなあ、と思います。

チュートリアルの大雑把な流れ

予めチュートリアルの内容というか、流れがどんなものか知っておいた方がいいと思うので、ザーッと書くと

  • 元々のプロジェクトはラスタライズ用に作られたプロジェクトHelloTriangleである
  • これにレイトレ用のコードを少しずつ追加していくことでラスタライズではなく、DirectXRaytracingを用いて三角形を描画する

いくつかトラップはあるものの、チュートリアル1およびチュートリアル2を「言われたとおりにコーディング」していけば、ラスタライズ時と同じように2Dの三角形が表示されます

チュートリアル2の時点だと2Dのため「視点」の概念はなく、スクリーンに平行にレイを飛ばして描画するものとなっている。

また、チュートリアル2の最後には発展的チュートリアルへのリンクが張られており、カメラの追加(ここで3D化する)、インスタンシングなどが書かれている。ここ、順番が正直前後しているため、カメラの追加→インスタンシングの順にやった方がいい(罠1)。

 

では実際にチュートリアルを見ていくとしよう。

Tutorial1の流れと罠

流れ

Tutorial1は基本的に「DirectXRaytracingを使用するための準備」までなので、この時点ではレイトレ用のシェーダも出てこないし、レイトレで何かが表示されたりはしない。ただし、7および8の冒頭で出て来る説明は、難解に思うかもしれないができる限りしっかり読んでおいた方がいい。理解できる範囲で理解して、理解できない部分はノートに書いておく(どうせ後から何となくわかってくるので、その時の理解の助けになる)

 

罠①

ただし、罠がある。先ほど「言われたとおりにコーディングすりゃいい」と書きましたが、あれは嘘だ。

まずヘルパー関数が罠で、これを落としてプロジェクトに追加するように言われるが、エラー出まくる出まくる・・・マジで?ってくらい出まくります。

これはチュートリアルにも書いてますが、プリコンパイルヘッダーは使用しないにしてください。チュートリアルでは該当cppだけって書いてますが、全部「しようしない」にした方がいいです。

で、ヘルパーのエラーは殆ど「必要なヘッダーが読み込まれてない事」が原因なので、それを潰します。

必要なヘッダーは

#include<stdexcept>

なので、持ってきたcppのほぼすべてにインクルードしましょう。これで大半のエラーが消えるはずです。そのほかにもvectorがないだのなんだのあるので、あとは自分で判断してインクルード追加しておいてください。

罠②

ここまでで、なんとかかんとかコンパイルを通して、言われたとおりにチュートリアル1を終えて実行した時にある事に気づく・・・ログにエラーが出まくっている。どういうエラーかというと、

「HitGroupTable.StartAddressは64バイトアライメントに揃えないとダメ!」

というものだ。これを無視しておけば後の悲劇は生まれなかったのだが、気になってしまい、

alignedAdrs += (64 - alignedAdrs % 64) % 64;

といった感じのコードを書いてしまい、これがチュートリアル2において悲劇を生むことになる。このためここは余計な事をせずに今はエラーを無視して進んだ方がいい。

この後の解説で、このエラーを片付ける方法(ヘルパー関数を修正する)を解説するのでここは我慢しよう。

 

チュートリアル2の流れと罠

チュートリアル2はいよいよレイトレのために必要な最低限のシェーダの登場である。

  • RayGenシェーダ
  • ClosestHitシェーダ
  • Missシェーダ

の3つです。Common.hlslもありますが、これは構造体的なのが定義されてるだけで、ヘッダとして用いるだけなので、DXR側から呼び出されるのはこの3つのシェーダです。

流れとしては、この3つのシェーダをロードし、をDXRパイプライン上で働くように登録していくという流れですね。

取り掛かる前に「Raytracing Gemsの第3章」のこの図を頭に入れておくと理解しやすいと思います。

f:id:boxer_programmer:20200923015012p:plain

とりあえずこのチュートリアルにおいては、RayGenシェーダ内でTraceRay()が呼ばれて、レイと三角形の衝突判定は勝手にやってくれてるようです。

なので、当たったらClosestHitシェーダが呼び出され、外れたらMissシェーダが呼び出される感じです。

罠③

これは元のチュートリアルじしんにも書かれてますが、ここに出て来るシェーダをVisualStudioのプロジェクトに含めてはいけません。コンパイルエラーが起きます。何故かというと、DXRをコンパイルするためにはFXCではなく、DXCでないといけないのですが、現時点のVisualStudio2019では、デフォルトがFXCなのです。このためプロジェクトに含めてしまうとビルドが止まってしまいます。

 

とはいえ、あとはチュートリアル通りに進めれば三角形が表示される・・・はず?

罠④

なのですが、いや、三角形は表示されてるのですが、色がついてないのです。これは罠②でいらん事やった(アライメントをそろえる)ために、本来Hitシェーダがあるべき場所がズレてしまい、Hitシェーダが呼ばれてなかったのです。しかし分かりやすいエラーがでるわけでもなく、かなり悩んでしまいました。

どういうことかというと、ヘルパ関数の中でMissの後にHitを配置しているのですが、その場所と、アライメント済みの場所が食い違っていても特にエラーが出ないため、まともに機能していませんでした。

 

罠④への対処

ではどう対処したのかというとヘルパ関数をいじりました。アライメント対処しなければまともに表示されたのですが、それではエラーログが出っぱなしになって気に入らなかったのです。

ShaderBindingTableGenerator.cpp内のGetEntrySize関数の中で

entrySize = ROUND_UP(entrySize, D3D12_RAYTRACING_SHADER_TABLE_BYTE_ALIGNMENT);

とし、ヘルパ関数内で64バイトアライメントになるようにしました。チュートリアルが作られた後にDXRの仕様が変わったんすかね?教えて詳しい人。

 

さて、そんなこんなで、ちまちま対処していけば、レイトレで三角形が表示されるようになります。

 

さて、この後ですが、やっぱり3Dにしたいと思います。チュートリアルではインスタンシングが先に書かれていますが、前述のとおり罠です。カメラパースペクティブを先に実装しましょう。

 

カメラパースペクティブに入るときの注意点

このチュートリアルでは右手系になっていますDirectXを左手系でプログラミングしている人はうっかりするとハマりますので注意しましょう(1敗)。

基本的にこれも言われたとおりに進めれば問題ないのですが、やっぱり1か所罠があります。

罠⑤

大した罠ではないのですが1.9の

ray.Origin = mul(viewI, float4(0, 0, 0, 1));

これは通りません。なぜならray.Originはfloat3のxyzだからです。正しくは

ray.Origin = mul(viewInv,float4(0,0,0,1)).xyz;

です。もっといい書き方があったら、誰か教えてください。

インスタンシング

次のインスタンシングですが、これは全然難しくないです。基本的には言われたとおりに進めればいいです。僕はわざと言われたとおりに進めなかったので色々ハマりましたが、余計なことしないほうが時間の無駄にならないと思います。

 

と、そこまで進めれば、レイトレで三角形と平面が表示され

f:id:boxer_programmer:20200923021741p:plain

こういうのが表示されます。全然シェーディングしてないので違和感ありますが、とりあえずDXRやったぜという達成感はありました。

ここでは解説は控えめにして、ハマりポイントだけを中心に紹介しました。それでは皆様よいレイトレーシングライフを・・・

 

計算幾何学

計算幾何学
昔、GFXというミドルウェアを使っていたのだが、Flashで作ったコンテンツをゲーム上で動かせるように…だけならそこまで珍しくもないだろうけど、そのハイレベルのテクニックによってハイスピードにFlashコンテンツを再生することができる。再生というのはムービーダダ流しでもなくGPUを効果的に使うことができる。

ありがたいことにソースコードも見ることができたが、非常にハイレベルだった。
悲しいことに、読んだときの俺のレベルでは解読不能であったため、猫に小判ではあった。

まぁ、僕が感動したのはそんなところでもなんでもなくて、フォントがあるじゃないですか、アウトラインフォント。
これをですね、ソフトウェア的に三角形分割してるんですよねぇ。アウトラインって結局ベジェなんで、
今の僕の技術でもできないわけじゃない。だけど、変にそこに感動した。

感動したので、計算幾何学とか勉強したし、したけど、チンプンカンプンで何回も投げ出したけど…。
結局ボロノイとか、ドロネー図に行っちゃったから難しくなってしまったのだと約10ヵ月後に気づいた。

これって、結局、ベジェをどうこう以前に、多角形の三角形分割なんだよね。ベジェを多角形に近似するのは
コンピュータがやってくれるので、それで得た頂点に対して、三角形分割を考えてやればいい。
多角形の三角形分割だけなら、コンピュータ・ジオメトリの中でも比較的簡単な問題である。

ただし、面倒なのは、凸多角形とは限らず、凹かもしれないし、穴あき(RとかAとか)かもしれない。
これもコンピュータ・ジオメトリに書いてあった。

現在ゆっくり実装中。

感動しているのは、あの、自分に実装できると思ってもなかった機能がいま、手の届く範囲にあるということだ。
30も半ばになってこれだ。これだからプログラマはやめられない。

C++のvectorとsetについて

いや、別にプログラミングの話じゃなくてね。思いついたことなんだけど
setって集合じゃん。とりあえずここではmultisetは置いておいて話するけどさ
これって要素を追加する際には、ソートしながら追加するわけですよね。
ってことは、setというかordered-setなわけで、まぁ、長いからsetにしたのかどーかは知らんけど、整列集合ってことにしておこう。

さて、んじゃ、vectorって、ベクトルだよね。でも、僕がベクトルと聞いてスグ思い浮かぶのは幾何ベクトル。つまり大きさと向きを持った量だよね。先生によっては平面状、空間上の移動を表すという先生もいるけどね。まぁ、そんな感じであって、Direct3Dとかの構造体D3DXVECTOR3とかが、そのイメージなんだろうね。ちなみにOpenGLって良く知らんけど、そういう構造体はあるんかね。なんか無さそうな気もする。

で、この「ベクトル」と、STLのベクトルは、どう見ても意味が違う。ということで、Wikipedia先生を見てみると、幾何ベクトルのほかに、「空間ベクトルの元」という説明もある。他のも沢山あるっぽいけど、多分この「ベクトル空間の元」とやらが一番それっぽいので、説明を見てみる。

すると

和とスカラー倍の定義された集合(代数系)のことである

らしい。なんのこっちゃ。まぁ、とりあえず集合の一つではあるので、特に次数は問わないんだろうよ。

Vは和について閉じているだのうんぬん書いてるけど、多分、集合を表そうとしたんだろうなぁ。
ところが、整列集合でsetって名前を使っちゃったので、とにかく要素を整列せず、重複を許すようかき集めたものを何か、setではない名前にしよう→vectorって流れなんかね。
作った人に聞かないとわからへんか・・・

僕の尊敬するおっぱいエンジニア

僕の尊敬するおっぱいエンジニアにあんどうやすしさんと言うエンジニアがおられます。

勿論面識はございませんが、とにかくおっぱいに情熱と技術を注ぎ込んでおられるお方です。

勝手にリンクさせていただきますが

http://d.hatena.ne.jp/technohippy/

です。最近はGoogleWaveを出版したり、Go言語にハマッたり、フォント作ったりしておられるようで、
おっぱいネタがなかなか出てこないのが残念だったりするのですが、

いやいや、おっぱいだからといって侮るなかれ。ハッキリ言って、ブログに書かれている理論が今の僕には分かりません。

分からないものがスゴイとか言ってるわけじゃなくて、実際に動いてるんだから…すごいといわざるを得ない。

えーとサイトに行ったら、左側のカテゴリーのOppaiをクリックしてください。
で、まずニコニコ動画を見て、うへっ、これどうやって作ってんの?と思う。
そしてずーっと下を見ると、何だかプログラムとか計算とか書いている。

http://d.hatena.ne.jp/technohippy/searchdiary?of=10&word=%2A%5BOppai%5D

バネ制約…


勉強すっか…

最も長い名前のSTLアルゴリズム

もっとも長い名前って、どれくらいやねん。

set_symmetric_difference

えーと24文字です。長いですね。STL系は単語と単語を_(アンダーバー)でつなぐからなおさらですね。

どういう意味のアルゴリズムかというと、一つずつ英単語を見ていきましょう。

setは集合って意味ですね。
symmetricは対称的だの対称性だのそんな意味です。
differenceは違いとか差とか言う意味ですが、集合なんで『差』でしょうね。

直訳すると、集合の対称差アルゴリズムです。

対称差ってなんやねんとWikipediaで調べてみたら、
「どちらか一方の集合には含まれるが両方とも含まれることがないような元を集めてできた新たな集合」

だそうです。式を描くと
PΔQ=(P∪Q)\(P∩Q)=(P\Q)∪(Q\P)
のようです。\は、左の集合から右の集合を削除したものといった意味です。

ベン図で描くと、PまたはQから、PかつQの領域を省いた集合となります。

長々と書きましたが、それをやってくれちゃうのが、この長い名前のアルゴリズムです。

…まぁ、ここまでわかっちゃうと、もう、解説することがないんですけど、注意点としてそれぞれの集合はソート済みであることが事前条件として要求されます。

ちなみに戻り値は、結果の末尾を表す反復子です。

サンプルコード書きます。ここでは集合はあらかじめソート済みなの用意しときます。
非ソート済みならば、sort関数等でソートしてから使ってください。配列長はめんどいので決め打ちにしてます。実際に使う際には必要に応じて配列長をはじき出してください。


サンプルコード


#include
#include
#include

using namespace std;


int main(){
int set1[] = {1,3,5,7,9};//集合1
int set2[] = {1,2,3,4,5};//集合2

//とりあえず集合のでかさがわかってるんで、多きさ10のベクタ用意しとく
vector set_result(10);

//あらかじめでかさが分からない場合には挿入子を使うのもいいかもしれません。
vector::iterator ret_it = set_symmetric_difference(set1,set1+5,set2,set2+5,set_result.begin());

vector::iterator it= set_result.begin();
vector::iterator itEnd = set_result.end();
//endまで見てみる
for(;it!=itEnd;++it){
cout << *it << ",";
}
cout << endl;

//アルゴリズム関数の戻り値=末尾なので、それを利用してみる
for(it=set_result.begin();it!=ret_it;++it){
cout << *it << ",";
}
cout << endl;
return 0;
}

出力結果
2,4,7,9,0,0,0,0,0,0,
2,4,7,9,

え?コンパイラがないですか?そんな人は、http://codepad.org/を活用すると面倒な手間をかけることなくテストできますので、是非活用しましょう。