rsqrtの精度とレイトレの不審なアーティフアクト

だいぶ前(前の冬なので半年以上前)に、Ray tracing in one weekendを読みながらRustでレイトレを実装していたのだが、そこでrsqrtを使ったら画像が変になったのを思い出した。理由は今も(ちゃんと調べていないので)よくわかっていないが、とりあえず思い出せるようにしておこうと思う。

rsqrtと呼んでいるのは以下の手法のことで、1 / sqrt(x)の形の計算を高速に近似するアルゴリズムだ。これは普通に計算すると、sqrtも重ければ割り算も重いので二重に重い。ベクトルを正規化したり長さを計算することが多いレイトレーシングやシミュレーションではこの計算が至る所に出てくるので、それを少しでも速くしたいというわけだ。

en.wikipedia.org

これは浮動小数点数の仕組みを上手く使った面白いトリックなので、興味があれば「Algorithm」のところを追ってみて欲しい。

ところで、今のCPUにはこのためだけの命令がある。

software.intel.com

なので自分でこのトリックを実装するよりもこれを直接呼んだ方が速いだろう。多分。

というわけでレイトレを実装したときにこれを使ってみた。このコードがそれに当たる。

github.com

以下の画像がrsqrtを使わずに素直に1/sqrt(x)を使ったものだ。

f:id:tniina:20191122195710p:plain
Fig.1 rsqrtなし

基本的にコンパイラは精度が落ちるような最適化を勝手にはやらないので、最適化の過程でこれが勝手にrsqrtにされるということはない。g++なら-ffast-mathを使うと勝手にやるが、rustcだと多分等価なオプションはないと思われる。議論はしてるようだが。

internals.rust-lang.org

調べてたら「rustcからLLVM IRを出してそれぞれ好きに最適化してあとでリンクしろ」と言っているストロングパーソンがいた。

閑話休題、上の画像に対して以下がrsqrtを使ったもの。画面中心付近に円形のアーティフアクトが出ている。結構広範囲にうっすらと広がっていることが見て取れる。

f:id:tniina:20191122195737p:plain
Fig.2 rsqrtあり

ど真ん中から同心円状にノイズが走っているので、レイの長さ関係の誤差が原因だろうなということはすぐにわかった。rsqrtをなくすとこのノイズも消えるので、実際そうらしい。

Rayは起点と方向の2つのベクトルから構成されているのだが、方向のベクトルは正規化されているという前提で残りのコードを書いているため、Ray::newは方向ベクトルを常に正規化する。ここでrsqrtが使われていて、そして多分そこで生じる誤差が原因なんだろうなとは思う。

原因がわからないのは気持ちが悪いので、これが原因でいくつかのピクセルが暗くなる理由を少し想像してみた。真っ先に思いついたのは、衝突判定して衝突点を返す時にレイの長さがおかしいせいで衝突点がオブジェクト内部にめり込んでしまって、反射した次の瞬間に同じオブジェクトにぶつかって暗くなる、というものだ。というわけで、衝突点がめり込まないように衝突距離を少しだけ短くしてみた。とりあえず真ん中の球体でその効果がなくなるまでやったのだが、1%を超えるレベルでずらしたため、今度は透明なオブジェクトの描画が完全に壊れてしまった。

f:id:tniina:20191122203723p:plain
Fig. 3 rsqrtで衝突位置を手前に補正(正ではない)

一応真ん中のLambertな球体からは例のアーティフアクトは消えている。最初の直感は正しかったのかもしれないが、もう少ししっかり調べないとまだ確信はできない。なんにせよ、透明なオブジェクトの描画が狂ってしまうならこのワークアラウンドは使えない。

どちらかというと、拡散反射のオブジェクトはここまで距離に無理な補正をかけてもそれっぽく見えてしまうのかと驚いた。全部を普通に実装しているものと見比べると明るさなどに若干の差があるが、並べないとわからない程度の差だ。壊れていることがすぐにはわからない場合、レイトレのデバッグは地獄かもしれない。いや、プロは収束すべき画像が見えるようになるのかもしれないが。