Haskellでsmallptを作ってみた

こんにちはtatsyです。

お盆休みなので最近勉強しているHaskellでsmallptを実装してみました(←暇か!)。

作成した手順に従って、ちょっとずつ解説してみたいと思います。
 

Vecクラス(のようなもの)を作る


「クラスのようなもの」と書いたのはHaskellにはオブジェクト指向型言語で言うところのクラスはないからです。Haskell的には独自定義のオブジェクトは代数的データ型といいます。

ちなみに、Haskellでclassというのは型クラスと呼ばれる他の言語でいうところのインターフェースのようなものを指します。まぎらわしいですね。

それはともかく、コードを見てみます。

まず、最初の数行でVec型の雛形となる定義を書いています。Vec型は足し算とか引き算とか出来てほしいのでNum型クラスを実装してあげます。

ちなみにabsとかsignumは使わないので適当にundefinedにしておきます。

その下は普通の関数定義です。ちなみに、ここではdataを使って新しい型を作っていますが、tupleに対してnewtypeを使ったりしても基本は同じです。
 

RayとSphereの定義


続いてはRayとSphereを定義します。こちらは単純な代数的データ型の定義なので特に難しいことはなしです。

続いてはintersection関数の定義です。これはSphereとRayの交点を求める関数ですが、smallptの関数をナイーブに実装すると次のようになります。

まあ、交わらないというのをMaybeモナドのNothingとかで表現してもいいんですが、今回はそこまでしなくてもいいかなという感じで、ナイーブに書いてあります。
 

radiance関数での乱数の取り扱い


今回の実装で一番ハマったポイントといっても過言ではないのがradiance関数における乱数の取り扱いです。

Haskell使いの方々には常識かと思うのですが、Haskellで普通に乱数を使おうとするとIOモナドが出てきます。

一応、解説しておくと、IOモナドというのはHaskellにおいて入出力を行うためのもので、IOを行うと参照透過性が損なわれることから、関数からの戻り値が普通の値ではなくIO Vecのようになります。

で、何が問題かというと乱数を取り扱う関数もIOを返してくるんですね。まぁ、乱数には参照透過性はないわけなので当たり前といえば当たり前なのかもですが。

別にIOのまま扱っても良かったのですが、レンダリング結果の画素値をIO Vecで持つようにして関数を書くと、IOの中身が積極評価されてメモリをたくさん食ってしまうみたいなのでした。

具体的には

みたいなリスト内包表記を使っていたのですが、こいつがメモリをバカ食いしてしまたのです。で、評価を遅らせるために、とりあえず

としてやったわけです。これでめでたくメモリのバカ食いは阻止できたのですがunsafePerformIOは何ともいえない感じだなぁと。

で、困ってStackOverflowで聞いてみたところMonadRandomという乱数を扱うためのモナドがあるらしい!

こいつを使って実装したradiance関数は次のような感じになります(長いです)。

はい、もうifelseがネストしまくって死にそうですね。時間があったらどうにかします。
 

実行結果


さてさて、これで主要な部分は説明できたはずですので、結果を見てみます。

haskell-smallptの結果

はい、確かにレイトレできてますね。素晴らしい!

まとめ

Haskellでレイトレするとかいう変なものを作って、誰が読むんだ?という気もしなくはないですが、一応目標達成です。

全体のコードはいつも通り自分のGitHubに上げておきますので、宜しければご覧ください。

https://github.com/tatsy/haskell-smallpt.git

最後までお読み頂きありがとうございました!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください