3DCG

Python3で「OpenGL」を学ぶ 「PyOpenGL」PyCG lesson7

lesson7を学んだ。順にそれぞれ自分の理解をメモしながらまとめる。

レイトレーシングとは、フォトリアリスティックな写真をレンダリングする方法。この記事ではこの概念を利用して、球体のスクリーンに投影するイメージを計算する簡単なレイトレーサーを作ってみる。今回はOpenGLは使用しない。

レイトレーシングアルゴリズム

レイトレーシングの背景の考えはシンプルである。膨大な計算量を節約するために、シーンを逆の順序でレンダリングしようとする。

まず前提として、次の場合ほとんどの計算が無駄になる。光源は無限の量の光線を放出し、シーン内のオブジェクトと相互作用する。最終的に、光線の一部はカメラに入り、画像としてキャプチャされる。しかし、このアルゴリズムがこのように実装されている場合、カメラに捕捉される光線のごく一部しかないため、非常に非効率的だ。

レイトレーシングのアイデアは、シーンを逆の順序でレンダリングしようとする。画像の各ピクセルに対して、プリミティブレイを生成し、シーン内の残りのオブジェクトに対してテストする。交差が見つかった場合、シャドウレイが生成され、それが別のオブジェクトの影の中にあるか、光源によって放射されているかを確認する。このようにすることで、膨大な計算量を節約できる。

レイトレーシングは、実装が最も簡単なレンダリングアルゴリズムの一つである。(スキャンラインレンダリングを使って画面上の線などの単純なジオメトリオブジェクトをレンダリングすることは容易ではない。

レイトレーシングの擬似コードと、交点と法線の計算

元記事のThe ray tracing algorithm に レイトレーシングの擬似コードがある。

また、デモでは三角形と球体の2種類のオブジェクトを実装した。これらは簡単な数式を持つジオメトリオブジェクトで、非常に簡単に扱うことができる。詳しい式は、The computation of intersection and normal を参照すること。

オブジェクト指向のアプローチ

CGのプログラムは、オブジェクト指向のパラダイムに最適な例の一つである。なぜなら、対話する三角形や球体などのオブジェクトが、すべて似たようなインターフェースを共有しているためだ。

例えば、交差を計算するintersectメソッド、表面の法線を計算するnormalメソッド、特定の照明設定の下で色を取得するshadeメソッドをすべて提供しなければならない。

OOPを用いることで、これらの共通メソッドを持つ基底クラスをすべてのオブジェクトに継承させることができ、プログラム設計の難易度を大幅に下げることができる。(Pythonでは動的型付けの関係上、継承を明示的に使わないようにしている。)

デモ

コードはGitHubを参照。計算を容易にするために、ThreadPoolクラスを使用してCPUコア間で計算を分散させている。結果はmatplotlibで表示している。記事の環境(Intel Core i7-8700K)では、800×600の画像をレンダリングするのに約20秒かかったとのこと。

また、自分の環境では、そのままでは動かなかったため、以下の3つの対応を行った。matplotlib がない場合は以下でインストール。

pip install matplotlib

Tutorial_7が見つからないなどの場合は、lesson5と同じように、main.pyの初めにコードを追加。

また以下のようなエラーが出る場合がある。

RuntimeError:
An attempt has been made to start a new process before the
current process has finished its bootstrapping phase.

This probably means that you are not using fork to start your child processes and you have forgotten to use the proper idiom in the main module:
if __name__ == '__main__':
freeze_support()
...
freeze_support()を調べると解決できるが、main.pyの初めに以下を追加。
import multiprocessing as mp

最後に、main.pyの最後のpoolより下を以下のように書き換える。雑ではあるが、Poolより前にmp.freeze_support()を呼べば良い。

if __name__ == '__main__':
    mp.freeze_support()
    pool = Pool(concurrency)
    allResults = pool.map(ray_trace_func, splitPxInds, chunksize=1)
    allResults = np.asarray(allResults)

    result = np.sum(allResults, axis=0)
    result = (np.clip(result, 0.0, 1.0) * 255.0).astype(np.uint8)
    plt.imshow(result)
    plt.show()