こんにちわ,Pocolです。
先週,1日有休を頂いて金・土・日の3日間でレイトレ合宿に参加してきました!
カテゴリー: レイトレ合宿!!
レイトレ合宿!!に関係するお話。
レイトレ合宿の季節ですね!
こんばんわ、Pocolです。
レイトレ合宿の季節になりましたね。
そういえば,去年のレイトレ合宿については何も書いていないような気がしたので,書いておきます。
昨年の結果は下記ページから確認できます。
https://sites.google.com/view/rtcamp9
私の去年のテーマは「GGXで描画する!」というものでした。
完全な鏡面反射では面白みがないので,すこしグロッシーな反射を描画したい。…と思いレンダラーを書いてみました。
ソースコードは下記にあります。
ponzu
実装としては,DirectX RayTracingを用いています。
デノイザーはクロスバイラテラルフィルタを使う単純なものです。
アセットはKenny Blaster Kitを拝借しています。
プログラム作成はかなりトライアルアンドエラーを繰り返すので,昨年からシェーダリロードに対応するようにしました。
今年はもうひとつ進めてblinkを用いたC++側のホットリロード対応しようかなって思います。
PNG出力はやっぱりfpngが圧倒的に速いので,昨年も用いました。恐らく今年も用いると思います。
https://github.com/richgel999/fpng
シーンデータはFlatBuffersを利用しており,デシリアライズの時間が短くなるようにしています。
https://github.com/google/flatbuffers
さて,今年開催されるレイトレ合宿10ですが,ルールや実行環境は下記の通りだそうです。
https://github.com/shocker-0x15/rtcamp10_setup/blob/main/exec_env_spec.md
今年は,256秒で描画しないといけないそうです。出来るだけ無駄なくプログラムを組まないといけないですね。
今年の参加エントリーはもう締め切られているため,レイトレ合宿への参加に興味がある人は,日頃からレイトレのプログラム画像をX(旧Twitter)などにあげると良いかもしれません。
ReSTIRがサッパリ解からない。
この記事はレイトレ合宿9のアドベントカレンダー2023 7/13の担当分の記事です。
こんにちわ,Pocolです。
最近,ReSTIRを調べているのですが,未だにサッパリ解かりません(特にGRIS周辺)。
今回は,自分の理解を促すために自分なりにまとめたスライドを作ってみました!
しかし,内容をサッパリ理解していない人が作っているスライドなので内容が盛大に間違えている場合があります。
間違いがあった場合は,間違い箇所の指摘とその修正例を教えて頂けると大変ありがたいです。あるいは,「そういう説明よりもこういう説明の方がいいよ」とか「この項目の説明抜けてる!」とか,アドバイスも貰えると嬉しいです。
または,有識者が分かりやすい解説記事を書いてもらえると大変助かります。
あと,論文の内容を切り貼りしている関係で,数式の表記ゆれがあります。予め注意してください。
レイトレ合宿8に参加してきました!
こんにちわ。Pocolです。
先週,レイトレ合宿8に参加してきました!
今年の開催地は沖縄ということで,台風11号が迫る中参加してきました。
台風が来ているせいで,まず現地に行くのが通常よりも難度が高いw。
金曜日到着の前泊組は問題なかったそうなのですが,自分は土曜日到着組だったので,
羽田空港から出発する便が,条件付き飛行になってました。
空港でも,「機長が着陸が難しいと判断した場合は羽田空港に引き返す恐れがありますので,予めご注意ください。」とアナウンスされている状況。
台風だからだと思うのですが,乗客も非常に少なかったです。でも,こんな中でも乗る人意外といるんだなと思ってびっくりしました。
しかし,タクシー載るころには豪雨状態。
さらに風も強くなってきたので,傘だと風でひっくり返ってしまってダメですね。
レインコートも持って行ったのですが,山用のやつだったので,夏だと暑いですし,上下セパレートしているので装着も面倒で結局着ずじまいでした。
ポンチョみたいな簡単に着れるやつを今後常備しておこうと思います。
さて、今年のレイトレ合宿の宿はVilla VALISOAで,これが滅茶苦茶良かったです。
詳細はWebページ見て欲しいのですが,写真詐欺も無く本当にこのままで,非常に綺麗でした。水着を持っていけばジャグジーやプールも入れます。
バスとトイレ・洗面所が隣同士なので,誰かが風呂に入っている最中はトイレを利用できなくなるというのが唯一の不便なところでした。2階にもバス・トイレ・洗面所があるので,そちらを利用することで回避も可能です。
ちなみに1階はガラス張りで外が見えます。ブラインドもついているので,下げた方がいいです(浴室隣の部屋から見えてしまいます)。
朝食等は2階にキッチンがついているので,各自で作るという感じのスタイルだそうです。食器等も一式揃っています。
2階から見える景色も非常に良かったです。台風が来ていなければ素晴らしい景色だったんだろうなと想像されます。
また,沖縄に行く機会があれば,再度利用したいなと思いました。
宿の話はこれぐらいにして…
今年はセミナーが非常に多く充実していました。
- ishiyamaさんの光線のなりたち
- yumcyawizさんのIntroduction to volume rendering
- Kinakomoti2357さんのPathtracingにおけるCausticsレンダリング
- 8picozさんのRustとGPU
- ykozwさんのAll about golden ratio
- うしおさんのNeural Radiance Fields to Implementation
…などなど。
ちなみに自分は,キャラクターレンダリングについてお話させて頂きましたが,大した内容ではないので一般公開する予定はありません。もうちょい話せるネタが溜まったら,どこかのイベントで話せるときが来るといいなーって思っています。
飛行機に乗っている時間にあったセミナーについては聴くことが出来なかったので,Zinさん,うしおさん,Shockerさんの生のプレゼンを見れなかったのが非常に悔やまれます。あとで資料をじっくりみたいと思います。
今回,セミナーで収穫があったのは,きなこ餅さん,PathtracingにおけるCausticsレンダリングについての資料で,Manifold Next Event Estimationの説明があったことです。これ論文見てもよーわからんなーって思っていたのですが,非常にわかりやすい説明で,そういうことだったのか!と納得できました。これで一気に論文が読みやすくなりました。
昼飯は車で少しいったあたりにある タコライスcafe きじむなぁ恩納村店 でタコライスを頂きました。
昼飯を食べ後は,デノイズ部門と本戦です。
まず,今年新設されたデノイズ部門でしたが,人によって異なる特徴を持つデノイズ結果が出ており,非常に興味深くそこが面白かったです。自分は余力が無かったため,エントリーしなかったのですが,次回開催するときもデノイズ部門があるようであれば,エントリーしてみたいなと思っています。
デノイズ部門参加者はみんな機械学習系が来るだろうと構えていたみたいですが,意外とみんな普通に書いていたみたいです。
決勝はykozwさんとholeさんとyumcyawizさんの戦いでした。
ちなみに,holeさんとyumcyawizさんは,ベース手法としてNL-Means (Non Local Means)フィルタを使用していて,接戦でした。
ykozwさんの手法は,明るさなどが他の二人と比べていてリファレンスに近い明るさが出ているなど,優れている部分もあり決勝は投票が非常に難しかったです。
今回のデノイズ部門はみんな投票悩んでいる感じで,会場にいて全体見るとこっちがよさげなんだけども,部分的にみるとなぁとか,自分も同じ感じで悩んだりしていて,その雰囲気も面白かったです。
今年の本戦ですが,色々な作品があって面白かったです。
意外とみんなカットとか変えないだろうなと予想していたのですが,やはりレンダリング野郎の集いなので,色々と凝っていてレベルが高かったです。
自分は今年は,GPUレンダラー作ったことが無かったので,とりあえず動くものを作れれば順位とか気にしないという,スタンスでレンダラー書きました。
テクスチャアニメーションとかリアルタイムっぽいのを使う人はいないだろうと予想したので,特徴としてテクスチャスクロールするだけのテクスチャアニメーションを入れました。本当は綺麗なコースティクスを出したいなぁと思ったのですが,提出2日前に全然バグの原因がわからず,そのまま提出したのであんまりレイトレ感が無いので,次回はちゃんとレイトレ感がある作品を提出したいなぁと思います。
あと,提出間際にシーンを変更したりとか色々とやっていた関係で,もとのIBL画像がバグって表示されるという残念な結果になるのを全然確認できていなかったので,このあたりもきちんとデバッグしておこうと思います。
とりあえず,GPUレンダラーのベースがある程度できたので,バグを取り除いて来年も使えるレンダラーに育てていきたいなと思っています。
Shcokerさんとかうしおさんとかの作品見ていると新しい技術取り入れていていいなぁーって思いました。新しい技術は積極的に取り入れていきたいです。
今年ビビったのが,holeさんの作品で見ると分かるのですが,ノイズが無いです。これが一番,驚きがありました。しかも書いたのが提出日当日とかって化物じみていてすげぇなぁっていう感想しかないです。
今年の結果見ると,次回のレイトレ合宿の本戦はどんなハイレベルな戦いになるのか予想が全くつきませんね。
次回も非常に楽しみです!
こうした楽しいイベントに参加できるもの,きちんと運営してくださる方があってのことなので,非常に有難いです。いつも本当にありがとうございます!
もし,レイトレ合宿の次回開催に参加したい人はTwitterで自作レンダラーの画像をいっぱいアップロードしておくと良いかもしれません。
レイトレ合宿楽しいぞ!! 君もレンダラーを書いてみないか?
以上です。
レイトレ合宿6に参加しました。
こんにちわ。
Pocolです。
今年もレイトレ合宿に参加しました。
今回の開催地は神津島で,海がもの凄く青くてとてもよかったです。
昼はholeさん達とバーガー食いに行きました。
バケツポテトは残念ながら上げ底されています。
夕方になると,ホテルからは夕陽が見られ,とても綺麗でした。
さて,レンダリングの方ですが
順位はブービーでしたが,エースコンバット5好きなので,ブービーでもいいかなと思っています。…というのもの、今年も準備が間に合いませんでした。個人的には順位はどうでもいいんです。参加することに意義がある!!
ちなみに描画はこんな感じです。
いくつかのフレームで黒画面が出るのはレンダリングが間に合っていないという仕様です(設計バグ)。
本当はボリュームレンダリングをする予定でしたが,間に合わず提出日当日になって,急遽方針を転換してパストレで行くことにしました。
最終提出日にエラーが発生してレンダリングされないなどの不具合があったので,レンダリングされただけもでも御の字かなと個人的には思っています。(本当黒い画像しか表示されないと思っていました)
Next Event Estimationの実装や交差判定処理箇所の設計見直しなど,1日で出来そうなことはとりあえずやりました。所々バグがあるのはまあ実力無さなので,そんなものでしょう。
今年はアニメーション部門に応募しました。
1フレーム12秒でレンダリングして60枚を出力することに目標にしました。
他の静止画部門の方々と比べるとレンダリング時間は1/10程度なので,ノイズまみれだし,特に何にもやっていないので汚いっちゃ~汚いです。(むしろ,こっちは10倍近くサンプルが少ないので,他の人の方が10倍近く綺麗じゃないとそもそもおかしい)
アニメーション部門に応募してみて,色々と課題とか問題があって難しいなと感じる部分を「面白いな」と錯覚してしまったので,来年もアニメーション部門に応募してみようかと思います。人と同じことをしていても面白みがないので,他の人がやらない分野には積極的に力を注ぐひねくれた人ですね。何よりも静止画よりも動画の方が個人的には血が騒ぎます。やりがいがあるし,やっぱり動くものは面白い。
あと今年はシークレットイベントとして,「レイトレ検定」が行われました。
ムズイ。#レイトレ合宿6 pic.twitter.com/PO4OCKYL8p
— Pocol (@ProjectAsura) 2018年9月1日
特に難しかったのはサンプリングを選ばせる問題です。皆さん,これ分かります?
自分は,6問中3問正解できたようでした。まだまだ勉強が足りませんね。
検定の最終結果は77点で,何とか赤点は免れることができました!
赤点は回避出来ました。#レイトレ合宿6 pic.twitter.com/9DbglynpAu
— Pocol (@ProjectAsura) 2018年9月1日
来年はもう少し落ち着いてレイトレ合宿に時間を割けると思うので,来年こそは頑張りたいなぁと思います。GPUレイトレーサー書きたいなぁ…
日々是精進ですね。
来年も是非参加したい!
レイトレ合宿運営の皆さん,どうもありがとうございました。
レイトレ合宿6Ω
こんにちわ。
Pocolです。
明日からレイトレ合宿6です。
今年の開催地は神津島ということで,ワクワクしています。
合宿終了後にレポートを上げる予定なので,
楽しみにしていてください。
毎年忘れるので
レイトレ合宿の季節ですね。
毎年,提出する際にDLLの同梱を忘れて実行できないことが多々あるので,忘れないようにメモっておこうと思います。
基本的にDependency Walkerで依存を見つけておきます。
次に,動作確認ですが自分の場合はまっさらなマシンで確認します。
さすがに物理マシンを用意するのは面倒なので,仮想PCを用意してWindowsをインストールした直後の状態で起動するかどうかチェックしています。
今回ハマったのは,dllを持ってくる場所。
最初はWindows/SystemWow64あたりから持ってきていたんですが,どうも起動しない。
ググってみたところ,VC直下から持ってこないとダメっぽいです。
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\redist\x64\Microsoft.VC140.CRT
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\redist\x64\Microsoft.VC140.OpenMP
からDLLを持ってきたところ正常に起動するようになりました。
起動に必要だったのは
msvcp140.dll
vcomp140.dll
vcruntime140.dll
の3つでした。
来年からは実行の際に忘れないように…。
レイトレ合宿に行ってきます。
今年もやってまいりました。
そうです,レイトレ合宿です。
今日・明日と2日間参加してきます。
今年は作品と呼べるものは作れなかった。
今年の失敗を忘れないようにメモとして残しておこうと思う。
そもそも,レイトレに関しての知識がゼロに等しいので,
去年まで作ってきたプログラムはバグが多く,今年は捨てて来年に向けたベースコードを作成することにした。
…でも提出3日前で下記のような状態。
本日の進捗。黒い画像が表示されるようになりました! pic.twitter.com/UBbvawYBar
— Pocol (@ProjectAsura) 2017年9月6日
何も表示されなくなって,かなり焦る。
原因はわかっているので,そのまま放置して実装を進める。
あと今年の提出2週間前のテストで,起動できない問題にハマった。たぶんDLLが問題。
OpenMP使っているとdllとか必要になるらしいので,脱OpenMP化した。
これで絵がでるはず!
…と思って,試しにレンダリングさせてみる。
何か絵は出たが,どうもおかしい。その時の描画結果が下図。
本日の進捗。乱数更新忘れててバグっています。 pic.twitter.com/LitU1joBsP
— Pocol (@ProjectAsura) 2017年9月7日
OpenMPの時はグローバル変数で参照していたのだが,流石にまずいだろうと思ってスレッドごとに別データにするようにしたら,上記のようなバグが出た。
で,上にも書いてある通りに乱数の更新をしないため,ずっと同じ値を参照していたようで変な結果になっていた。
乱数更新を計算後に入れ忘れていたので,更新するようにしたら解決。焦る焦る。
この時点で,まだメッシュは未対応。これが提出前日です。
ヤバい感じが出てきたので,そろそろメッシュを表示しようと思って対応を始める。
メッシュフォーマットを大きく変えるとめんっどちいので,昨年まで使っていたやつに修正を加えて,表示を試みる。
絵が出た!
進捗。メッシュが表示できるようになりました! pic.twitter.com/d0NFCvBSNa
— Pocol (@ProjectAsura) 2017年9月7日
よくみると,ちゃんと表示されている。
…が,たぶん普通の人には分からないだろうと思って,ネタとして投稿した。
ご覧のように,全く最適なしていないため計算がゲロ重くてレイが全然飛ばない。
結局前日は,BVHを8割ぐらい実装したところで,力尽きて寝た。
提出当日。
実家から自宅に帰る前にBVHを実装。いざ実行!
「…スタックオーバーフローだってさ」
またもや,バグる。
あぁもう!!なんで~
デバッグ時間が取れないまま実家を後にして,自宅に向かう。
移動時間で2時間をロス。
夜飯を買いに行く時間が無いと思ったので,帰り際にコンビニで昼食と夕食を購入し,自宅に到着。
BVHのデバッグを開始。
「うん!さっぱりわからない…」
時間をものすごく使いそうだったので,SAHの実装は諦め中間分割の実装に切り替え。こっちのほうが実装楽だし,時間が無いということで。
で,ようやくBVHが入る。
進捗。BVHをいれてメッシュの交差判定を高速化しました。残り5時間。 pic.twitter.com/G4r7wtRR1m
— Pocol (@ProjectAsura) 2017年9月8日
メッシュの位置を直すのもめんどいので,そのまま。
実装終えたところで,もう時間が5時間を切っている。
ここから,QBVHの実装に入る。
QBVHは一度躓いているので,サクッと実装できた。
で,実行してみると…
「なんか,普通のBVHよりも遅い」
以前実装したときは,BVHよりも確実に速かったのだが,どうも体感的に何も感じない。
で, またバグっていた。衝突判定の実装ミス。
無駄な計算をかなりやっているコードになっていた。焦ってコード書くとろくなことにならないね。皆さん落ち着いて書きましょう。
直したところで,実行。
時間計測するコードを書く時間も惜しいので,レンダリング画像を目視して確認。とりあえず,速くなっているっポイ。
続いて,OBVHの実装に移る。
こちらも,すんなり終わるかと思いきや…。
「なぜかビルドエラーが出る」
なんか,_mm256_cvtss_f32() が無いって怒られる。なぜだ?clangのドキュメントには書いてあるのに。
残り時間も3時間ぐらいになってきたので,ネットの情報を元に自前で関数追加してビルドエラーに対処。
自宅のメインPCがAVXサポートしていないので,サブに使っているノートPCで動作チェック。
とりあえず,絵は出るっポイ。
ここで,残り時間が2時間。
流石に,IBLをMISを入れるのは無理と判断。
最終レンダリング用のアセットをここから探し始める。一応,用意していたアセットはあるんだけども,あんまり見栄えしないのでボツにした。
で,もう時間もなく探している時間もないので適当に基本形状を配置することに。
プログラムが起動できない問題が解決できていないので,ようやくチェックすることに。
Visual Studioでexe起動時にログ出力されるdllを取り合えず片っ端から,batでコピーして実行ファイルに同梱してみたのだが,上の結果。
とりあえず,エラーを再現するために,Virtual BoxにWindows 7をインストール。
VirtualBoxで動作させてみたら,案の定同じエラーが出た。
で,結局DLLが問題っぽいので,DLLを使わないようにVCのビルド設定で「マルチスレッドDLL」になっている設定を「マルチスレッド」に変更して,軌道を試みる。
「お、出た」
これで,めでたく起動した!
この時点で,残り時間が既に1時間を切っている。
流石にそろそろzip用意しないとまずいので,提出用スライドの作成に入る。
前日に力尽きて寝る前に,色々とページ作っていたけど時間が無いし,今年は負け戦なのでアピールするべきことも無いのでバッサリ省略。
スライド作成中にレンダリングをさせておいて,最新のレンダリング画像を添付。
exeの起動を確認して,急いでzipに固めて提出。
残り時間20分。
レンダリング画像を見ると,やっぱり背景がどうも気に入らないので
HDR画像を差し替えて,シーンファイルも更新。
起動して,1枚目の画像が出力されるところまで確認して,大慌てでzip化して再び提出。
残り4分のところで,提出した。
— Pocol (@ProjectAsura) 2017年9月8日
残り4分だった。
来年はこういうことが無いように,ちゃんと準備したい。
上記で書いた以外にも,画像サイズを変えると描画結果がおかしくなるバグとか色々とあって,
そのあたり直したので,来年はもう少しまともなレンダリングで出来るはず
…であってほしいなぁ。
レイトレ再入門
この記事はレイトレ合宿5!?のアドベントカレンダーの4週目の記事です。
こんにちわ,Pocolです。
皆さんレイを飛ばしていますか?
さて,昨年のレイトレ合宿ではビリという結果を残してしまったので,もう一度一から出直そうと思いました。…ということで,レイトレ再入門と題して,勉強し直してみようと思います。
レンダーターゲットの用意
まずは,描画先が必要になるのでレンダーターゲットを用意します。
HDRを表現できるように倍精度浮動小数型のVector3クラスを使いました。
// レンダーターゲット生成. std::vector<Vector3> image; image.resize(width * height);
レンダーターゲットを作成したら,忘れずにクリアしておきます。
// レンダーターゲットをクリア. for (size_t i = 0; i < image.size(); ++i) { image[i] = g_back_ground; }
今までは,自分でビットマップクラスを用意していたのですが,さすがにダルくなってきたので,
stb_image_writeを使いました。
ビットマップ出力は次のような感じです。
//------------------------------------------------------------------------------------------------- // BMPファイルに保存します. //------------------------------------------------------------------------------------------------- void save_to_bmp(const char* filename, int width, int height, const double* pixels) { std::vector<uint8_t> images; images.resize(width * height * 3); const double inv_gamma = 1.0 / 2.2; for(auto i=0; i<width * height * 3; i+=3) { auto r = pow(pixels[i + 0], inv_gamma); auto g = pow(pixels[i + 1], inv_gamma); auto b = pow(pixels[i + 2], inv_gamma); r = saturate(r); g = saturate(g); b = saturate(b); images[i + 0] = static_cast<uint8_t>( r * 255.0 + 0.5 ); images[i + 1] = static_cast<uint8_t>( g * 255.0 + 0.5 ); images[i + 2] = static_cast<uint8_t>( b * 255.0 + 0.5 ); } stbi_write_bmp(filename, width, height, 3, images.data()); }
シーンを用意する
次に簡単なシーンを用意します。球が最もやりやすいので,eduptと同じように大きな球を用意してコーネルボックスを表現します。
データは次の通りです。
const Sphere g_spheres[] = { Sphere(1e5, Vector3( 1e5 + 1.0, 40.8, 81.6), Vector3(0.25, 0.75, 0.25)), Sphere(1e5, Vector3(-1e5 + 99.0, 40.8, 81.6), Vector3(0.25, 0.25, 0.75)), Sphere(1e5, Vector3(50.0, 40.8, 1e5), Vector3(0.75, 0.75, 0.75)), Sphere(1e5, Vector3(50.0, 40.8, -1e5 + 170.0), Vector3() ), Sphere(1e5, Vector3(50.0, 1e5, 81.6), Vector3(0.75, 0.75, 0.75)), Sphere(1e5, Vector3(50.0, -1e5 + 81.6, 81.6), Vector3(0.75, 0.75, 0.75)), Sphere(16.5, Vector3(27.0, 16.5, 47.0), Vector3(0.75, 0.25, 0.25)), Sphere(16.5, Vector3(73.0, 16.5, 78.0), Vector3(0.99, 0.99, 0.99)) };
上記で定義している球は次のようにコーディングしています。
/////////////////////////////////////////////////////////////////////////////////////////////////// // Sphere sturcture /////////////////////////////////////////////////////////////////////////////////////////////////// struct Sphere { double radius; //!< 半径です. Vector3 pos; //!< 位置座標です. Vector3 color; //!< 色です. Sphere ( double r, const Vector3& p, const Vector3& c ) : radius (r) , pos (p) , color (c) { /* DO_NOTHING*/ } inline double intersect(const Ray& ray) const { auto p = pos - ray.pos; auto b = dot(p, ray.dir); auto det = b * b - dot(p, p) + radius * radius; if (det >= 0.0) { auto sqrt_det = sqrt(det); auto t1 = b - sqrt_det; auto t2 = b + sqrt_det; if (t1 > D_HIT_MIN) { return t1; } else if (t2 > D_HIT_MIN) { return t2; } } return D_HIT_MAX; } };
交差判定はintersect()メソッドで行い,実装は2次方程式の判別解\(\frac{D}{4}\)を用いて,当たったかどうかを計算しています。
ここまででシーンデータが準備出来ました。
レイを飛ばす
いよいよレイを飛ばします。レイはカメラから飛ばすので,最初にカメラクラスを実装しておきます。
/////////////////////////////////////////////////////////////////////////////////////////////////// // Camera class /////////////////////////////////////////////////////////////////////////////////////////////////// class Camera { public: Camera ( const Vector3& position, const Vector3& dir, const Vector3& upward, double fov, double aspect, double znear ) { pos = position; axis_x = normalize(cross(dir, upward)) * fov * aspect; axis_y = normalize(cross(dir, axis_x)) * fov; axis_z = dir; near_clip = znear; } inline Ray emit(double x, double y) const { auto d = axis_x * x + axis_y * y + axis_z; auto p = pos + d * near_clip; return Ray(p, normalize(d)); } private: Vector3 pos; //!< 位置座標です. Vector3 axis_x; //!< 基底ベクトル(X軸) Vector3 axis_y; //!< 基底ベクトル(Y軸) Vector3 axis_z; //!< 基底ベクトル(Z軸). double near_clip; //!< ニア平面までの距離. };
やっていることはnear_clipをスクリーンの位置と見立てて,posを中心としたビュー空間を構成する基底ベクトルを求めます。
正規直交基底ベクトルを用いるのが普通なのですが,レイを飛ばすたびに同じ計算を行うと処理効率が悪いので,毎回同じ計算をする箇所は最初に1度計算してしまって,値を保存して使いまわすことにしています(axis_xとaxis_yのところが該当箇所です)。
ビュー空間を構成するベクトルが定まったら,emit()メソッドを使うことでレイを発射することができます。emit()メソッドでやっていることは,視点位置から,視線ベクトル方向に一旦移動し,xとyで指定されるスクリーン位置までaxis_xとaxis_yを使って移動します。このベクトルがレイを発射する方向となるので,正規化することでレイの方向ベクトルが定まります。レイの位置座標はカメラ位置から,方向ベクトルにnear_clip分だけ進んだところがスクリーンのヒット位置になります。この点を起点としてレイを発射します。
次に,レイと物体との交差判定を行います。すでにSphereクラスに交差判定用の処理があるので,これをシーンに配置されている球の数分for分で回して,一番交差距離が短いものを衝突物体として採用します。
//------------------------------------------------------------------------------------------------- // シーンとの交差判定を行います. //------------------------------------------------------------------------------------------------- inline bool intersect_scene(const Ray& ray, double* t, int* id) { auto n = static_cast<int>(sizeof(g_spheres) / sizeof(g_spheres[0])); *t = D_MAX; *id = -1; for (auto i = 0; i < n; ++i) { auto d = g_spheres[i].intersect(ray); if (d > D_HIT_MIN && d < *t) { *t = d; *id = i; } } return (*t < D_HIT_MAX); }
交差判定をして,ヒットしたら,そのピクセルに色を塗ります。
//------------------------------------------------------------------------------------------------- // 交差物体の色を求めます. //------------------------------------------------------------------------------------------------- Vector3 shade(const Ray& ray) { double t; int id; // シーンとの交差判定. if (!intersect_scene(ray, &t, &id)) { return g_back_ground; } // 交差物体の色を返却. return g_spheres[id].color; }
これで,ヒット判定も実装できたので,レイを飛ばしてみます。
実装は次のようになります。
//------------------------------------------------------------------------------------------------- // メインエントリーポイントです. //------------------------------------------------------------------------------------------------- int main(int argc, char** argv) { // レンダーターゲットのサイズ. int width = 640; int height = 480; // カメラ用意. Camera camera( Vector3(50.0, 52.0, 295.6), // カメラ位置. normalize(Vector3(0.0, -0.042612, -1.0)), // 視線ベクトル. Vector3(0.0, 1.0, 0.0), // 注視点. 0.5135, // 垂直画角(rad) double(width) / double(height), // アスペクト比. 130.0 // スクリーンまでの距離. ); // レンダーターゲット生成. std::vector<Vector3> image; image.resize(width * height); // レンダーターゲットをクリア. for (size_t i = 0; i < image.size(); ++i) { image[i] = g_back_ground; } for (auto y = 0; y < height; ++y) { for (auto x = 0; x < width; ++x) { auto idx = y * width + x; auto fx = double(x) / double(width) - 0.5; auto fy = double(y) / double(height) - 0.5; // Let's レイトレ! image[idx] += shade(camera.emit(fx, fy)); } } // レンダーターゲットの内容をファイルに保存. save_to_bmp("image.bmp", width, height, &image.data()->x); // レンダーターゲットクリア. image.clear(); return 0; }
実装出来たら,実行してみましょう。
キチンとヒットしていれば下記のように色がつくはずです。
ここまでの,実装プログラムをGithubにアップロードしておきました。
https://github.com/ProjectAsura/sample_hit
これでレイが飛ばせるようになりました。
古典的レイトレーシング
続いて,古典的レイトレーシングを実装してみます。マテリアルの概念を導入し,マテリアルに沿って反射レイの飛ばし方を変えてみます。
まず,反射タイプを追加します。
/////////////////////////////////////////////////////////////////////////////////////////////////// // ReflectionType enum /////////////////////////////////////////////////////////////////////////////////////////////////// enum ReflectionType { Diffuse = 0, //!< 完全拡散反射. PerfectSpecular = 1, //!< 完全鏡面反射. Refraction = 2, //!< 屈折. };
反射タイプを追加したら,shade()メソッドをradiance()メソッドにリネームして,下記のような実装を行います。
//------------------------------------------------------------------------------------------------- // 放射輝度を求めます. //------------------------------------------------------------------------------------------------- Vector3 radiance(const Ray& ray, int depth) { double t; int id; // シーンとの交差判定. if (!intersect_scene(ray, &t, &id)) { return g_back_ground; } // 交差物体. const auto& obj = g_spheres[id]; // 交差位置. const auto hit_pos = ray.pos + ray.dir * t; // 法線ベクトル. const auto normal = normalize(hit_pos - obj.pos); // 物体からのレイの入出を考慮した法線ベクトル. const auto orienting_normal = (dot(normal, ray.dir) < 0.0) ? normal : -normal; // 打ち切り深度に達したら終わり. if(depth > g_max_depth) { return g_back_ground; } switch (obj.type) { case ReflectionType::Diffuse: { double t_; int id_; // ライトベクトル. auto light_dir = g_light_pos - hit_pos; // ライトまでの距離. auto light_dist = length(light_dir); // ライトベクトルを正規化. light_dir /= light_dist; // ライトとの間に遮蔽物がないことを確認. intersect_scene(Ray(hit_pos, light_dir), &t_, &id_); // 遮蔽物がない場合. if (t_ >= light_dist) { auto diffuse = obj.color * max(dot(orienting_normal, light_dir), 0.0) / (light_dist * light_dist); return g_light_color * diffuse; } else { // 遮蔽物がある. return g_shadow_color; } } break; case ReflectionType::PerfectSpecular: { // 反射させる. return obj.color * radiance(Ray(hit_pos, reflect(ray.dir, normal)), depth + 1); } break; case ReflectionType::Refraction: { // 反射レイ auto reflect_ray = Ray(hit_pos, reflect(ray.dir, normal)); // 内部侵入するか? auto into = dot(normal, orienting_normal) > 0.0; // 空気の屈折率 const auto nc = 1.0; // 物体の屈折率 const auto nt = 1.5; // Snellの法則. const auto nnt = (into) ? (nc / nt) : (nt / nc); const auto vn = dot(ray.dir, orienting_normal); const auto cos2t = 1.0 - nnt * nnt * (1.0 - vn * vn); // 全反射かどうかチェック. if (cos2t < 0.0) { return obj.color * radiance(reflect_ray, depth + 1); } // 屈折ベクトル. auto refract = normalize(ray.dir * nnt - normal * ((into) ? 1.0 : -1.0) * (vn * nnt + sqrt(cos2t)) ); // Schlickによる Fresnel の反射係数の近似. const auto a = nt - nc; const auto b = nt + nc; const auto R0 = (a * a) / (b * b); const auto c = 1.0 - ((into) ? -vn : dot(refract, normal)); const auto Re = R0 + (1.0 - R0) * pow(c, 5.0); const auto nnt2 = pow((into) ? (nc / nt) : (nt /nc), 2.0); const auto Tr = (1.0 - Re) * nnt2; const auto p = 0.25 + 0.5 * Re; // 屈性レイ Ray refract_ray(hit_pos, refract); const auto reflect_result = radiance(reflect_ray, depth + 1) * Re; const auto refract_result = radiance(refract_ray, depth + 1) * Tr; return obj.color * (reflect_result + refract_result); } break; } // どれにもヒットしなかった. return g_back_ground; }
リアルタイムレンダリングのようにLambertのBRDFを計算しているのと,反射・屈折の処理が追加されています。
反射・屈折処理についてはeduptとほぼ同じなので説明はeduptのスライドを参照してください。
名前をradiance()に変えたのでmain()関数側も忘れずに修正しておきます。
for (auto y = 0; y < height; ++y) { for (auto x = 0; x < width; ++x) { auto idx = y * width + x; auto fx = double(x) / double(width) - 0.5; auto fy = double(y) / double(height) - 0.5; // Let's レイトレ! image[idx] += radiance(camera.emit(fx, fy), 0); } }
修正したら実行してみます。
きちんと実装されていれば次のようになるはずです。
この程度の処理なら,現代的なPCを使っていれば1秒かからないうちに終わります。
ここまでのサンプルプログラムをGithubにアップロードしておきました。
https://github.com/ProjectAsura/sample_rt
Path Tracing
さて,続いてパストレです。radiance()メソッドでは,ここまで再帰を使っていましたが,あんまり呼び出すとスタックオーバーフローになる可能性があるので,再帰を使わずにループ文に書き直します。実装は次のような感じです。
//------------------------------------------------------------------------------------------------- // 放射輝度を求めます. //------------------------------------------------------------------------------------------------- Vector3 radiance(const Ray& input_ray, int depth, Random* random) { Vector3 L(0, 0, 0); Vector3 W(1, 1, 1); Ray ray(input_ray.pos, input_ray.dir); while(true) { double t; int id; // シーンとの交差判定. if (!intersect_scene(ray, &t, &id)) { break; } // 交差物体. const auto& obj = g_spheres[id]; // 交差位置. const auto hit_pos = ray.pos + ray.dir * t; // 法線ベクトル. const auto normal = normalize(hit_pos - obj.pos); // 物体からのレイの入出を考慮した法線ベクトル. const auto orienting_normal = (dot(normal, ray.dir) < 0.0) ? normal : -normal; auto p = max(obj.color.x, max(obj.color.y, obj.color.z)); L += W * obj.emission; // 打ち切り深度に達したら終わり. if(depth > g_max_depth) { if (random->get_as_double() >= p) { break; } } else { p = 1.0; } switch (obj.type) { case ReflectionType::Diffuse: { // 基底ベクトル. Vector3 u, v, w; w = orienting_normal; if (abs(w.x) > 0.1) { u = normalize(cross(Vector3(0, 1, 0), w)); } else { u = normalize(cross(Vector3(1, 0, 0), w)); } v = cross(w, u); const auto r1 = D_2PI * random->get_as_double(); const auto r2 = random->get_as_double(); const auto r2s = sqrt(r2); auto dir = normalize(u * cos(r1) * r2s + v * sin(r1) * r2s + w * sqrt(1.0 - r2)); ray = Ray(hit_pos, dir); W *= (obj.color / p); } break; case ReflectionType::PerfectSpecular: { ray = Ray(hit_pos, reflect(ray.dir, normal)); W *= (obj.color / p); } break; case ReflectionType::Refraction: { Ray reflect_ray = Ray(hit_pos, reflect(ray.dir, normal)); auto into = dot(normal, orienting_normal) > 0.0; const auto nc = 1.0; const auto nt = 1.5; const auto nnt = (into) ? (nc / nt) : (nt / nc); const auto ddn = dot(ray.dir, orienting_normal); const auto cos2t = 1.0 - nnt * nnt * (1.0 - ddn * ddn); if (cos2t < 0.0) { ray = reflect_ray; W *= (obj.color / p); break; } auto dir = normalize(ray.dir * nnt - normal * ((into) ? 1.0 : -1.0) * (ddn * nnt + sqrt(cos2t))); const auto a = nt - nc; const auto b = nt + nc; const auto R0 = (a * a) / (b * b); const auto c = 1.0 - ((into) ? -ddn : dot(dir, normal)); const auto Re = R0 + (1.0 - R0) * pow(c, 5.0); const auto Tr = 1.0 - Re; const auto prob = 0.25 + 0.5 * Re; if (random->get_as_double() < prob) { ray = reflect_ray; W *= (obj.color * Re / prob) / p; } else { ray = Ray(hit_pos, dir); W *= (obj.color * Tr / (1.0 - prob)) / p; } } break; } depth++; } return L; }
次に,複数サンプルとれるようにmain()関数内の処理を次のように変更します。
//------------------------------------------------------------------------------------------------- // メインエントリーポイントです. //------------------------------------------------------------------------------------------------- int main(int argc, char** argv) { // レンダーターゲットのサイズ. int width = 640; int height = 480; int samples = 512; // カメラ用意. Camera camera( Vector3(50.0, 52.0, 295.6), normalize(Vector3(0.0, -0.042612, -1.0)), Vector3(0.0, 1.0, 0.0), 0.5135, double(width) / double(height), 130.0 ); // レンダーターゲット生成. std::vector<Vector3> image; image.resize(width * height); Random random(123456); // レンダーターゲットをクリア. for (size_t i = 0; i < image.size(); ++i) { image[i] = g_back_ground; } for(auto s = 0; s < samples; ++s) { printf_s("%.2lf%% complete\r", (double(s)/double(samples) * 100.0)); for (auto y = 0; y < height; ++y) { for (auto x = 0; x < width; ++x) { auto idx = y * width + x; auto fx = double(x) / double(width) - 0.5; auto fy = double(y) / double(height) - 0.5; // Let's レイトレ! image[idx] += radiance(camera.emit(fx, fy), 0, &random) / samples; } } } // レンダーターゲットの内容をファイルに保存. save_to_bmp("image.bmp", width, height, &image.data()->x); // レンダーターゲットクリア. image.clear(); return 0; }
見ると分かるように,乱数が導入されているのと,サンプル数分ループする処理が追加されています。
このプログラムを実行すると次のような結果が得られます。
サンプル数が少ないため,ノイジーですが,ガラス玉付近に集光現象が見られたり,柔らかい影が表現されていたりなど,ライティング結果の向上が見て取れます。
ここまでのプログラムをGithubにアップロードしておきました。
https://github.com/ProjectAsura/sample_pt
これでパストレーシングも実装しました。
Next Event Estimation
次に速度向上のために直接光ライティングを導入してみます。
Next Event EstimationについてはSchokerさんが詳しく説明されています。Schokerさんのページを参照しましょう。
http://rayspace.xyz/CG/contents/path_tracing_implementation/
物体と衝突したら,光源方向にシャドウレイを飛ばします。シャドウレイを飛ばした結果,ライト以外の遮蔽物と交差していなければライトの寄与が取れます。遮蔽されている場合は何もしません。今回のサンプルではライトが一つなので,1つのライトを選択して,乱数により適当なライト表面上の位置を決定します。この点に向かってシャドウレイを飛ばして,交差判定を行います。遮蔽が無い場合は,BRDFと,G項とライトの確率密度関数から寄与を計算します。G項については下記のスライドに記載があります。
このNext Event Estimationの処理をDiffuse計算の先頭に追加します。
case ReflectionType::Diffuse: { #if 1 // Next Event Estimation { const auto& light = g_spheres[g_lightId]; const auto r1 = D_2PI * random->get_as_double(); const auto r2 = 1.0 - 2.0 * random->get_as_double(); const auto light_pos = light.pos + (light.radius + D_HIT_MIN) * Vector3(sqrt(1.0 - r2 * r2) * cos(r1), sqrt(1.0 - r2 * r2) * sin(r1), r2); // ライトベクトル. auto light_dir = light_pos - hit_pos; // ライトへの距離の2乗 auto light_dist2 = dot(light_dir, light_dir); // 正規化. light_dir = normalize(light_dir); // ライトの法線ベクトル. auto light_normal = normalize(light_pos - light.pos); auto dot0 = dot(orienting_normal, light_dir); auto dot1 = dot(light_normal, -light_dir); auto rad2 = light.radius * light.radius; // 寄与が取れる場合. if (dot0 >= 0 && dot1 >= 0 && light_dist2 >= rad2) { double shadow_t; int shadow_id; Ray shadow_ray(hit_pos, light_dir); // シャドウレイを発射. auto hit = intersect_scene(shadow_ray, &shadow_t, &shadow_id); // ライトのみと衝突した場合のみ寄与を取る. if (hit && shadow_id == g_lightId) { auto G = dot0 * dot1 / light_dist2; auto pdf = 1.0 / (4.0 * D_PI * rad2); L += W * light.emission * (obj.color / D_PI) * G / pdf; } } } #endif
この追加を加えたプログラムを実行すると,次のような結果が得られました。
Next Event Estimationが入っていないプログラムに比べると良くなっているのが見て取れます。
ここまでのプログラムをGithubにアップロードしておきました。
https://github.com/ProjectAsura/sample_pt2
若干,プログラムが怪しいですが…
一応Next Event Estimationが実装できたということにしておきたいと思います。