WebGL+GLSLによる超高速なパストレーシング

  • 48
    いいね
  • 0
    コメント

これは WebGL Advent Calendar 2016 の25日目の記事です。

トリの記事を担当できて、とても嬉しいです!今年のWebGLアドベントカレンダーは過疎ってしまったので、2017年はもっと盛り上げていきたいと思っています!!


ブラウザ上で動作するパストレーサを three.js( WebGL ) + GLSL で実装しました。

パストレーシングは大域照明を考慮した実写のような品質の高い結果を得られる反面、非常に計算コストが大きいレンダリング手法です。今回はこのパストレーシングをGPUで実装することで大幅に高速化し、ほぼリアルタイムなプレビューを実現しました。

次のリンクから動作を確認できます。携帯端末では動作しない1ので、PCからご覧ください。

動作は、次の動画からもご覧いただけます。

機能紹介

シーンエディット

図: シーンエディットでオブジェクトを選択した状態

このパストレーサにはシーンエディット機能があります。

上の図で、緑色のバウンディングボックスで囲まれているオブジェクトは選択中の状態になっています。
この状態では、選択中のオブジェクトの位置やスケール、マテリアルをリアルタイムに編集・プレビューできます。

マテリアルの設定項目には、物質が透明かどうか、Color(物体の色)、GGXのRoughness(物体の表面の粗さ)、Emission(物体が放つ光)があります。

ちなみに、Color / Roughness / Emissionにはテクスチャを指定することもできます。
例えば、先の選択中のオブジェクトでは世界地図のテクスチャをEmissionとして指定しています。また、奥の板状のガラスの箱ではRoughnessをテクスチャに指定しています。

環境マップを変更した例

図: 環境マップを変更した例

環境マップによるIBLをサポートしており、GUIのenvMapから環境マップを変更できます。
環境マップを変更すると、同じシーンとマテリアルでも印象がぐっと変わるのはIBLのおかげです。

背面の世界地図を光源にした例

図: 背面の世界地図を光源にした例

背面の世界地図を光源にすると、右のガラス面や磨りガラス面の球体でコーティクスを確認できます。

球体をズームした例

図: 球体をズームした例

ズームしてみると、設置部分のAO(Ambient Occlusion)や床への球体の赤色の映り込みを確認できます。

テーブル状に箱をスタックした例

図: テーブル状に箱をスタックした例

GUIのScene Edit > Presetからシーンのプリセットをロードできます。上の図はtableというプリセットをロードした様子です。

この記事では自作のパストレーサの機能の紹介と実装ついての解説を行います。

パストレーシングとは?

パストレーシングはモンテカルロ法を用いてレンダリング方程式の解を求めるレンダリング手法です。大域照明を考慮した実写のような高品質な結果を得られる反面、非常に計算コストが大きい手法です。

モンテカルロ法では、乱数をつかってシミュレーションを何回も繰り返し行い、その平均から解を求めます。この1回のシミュレーションのことをサンプリングとも呼びます。処理コストが大きい理由は、この複数回のサンプリングが必要なためです。

パストレーシングについての詳細はこの記事では説明しませんが、@shocker-0x15 さんによるパストレーシングについて丁寧に紹介されています。

パストレーシングの実装

今回のパストレーサでは、カメラを動かすとシミュレーションの結果を捨ててしまいます。そのため、カメラを動かした直後はノイズだらけの結果になりますが、しばらく待つと結果が収束して綺麗な結果となります。

パストレーシングを実装の概要を箇条書で示すと、次のようになります。

  1. 描画の1フレームごとにパストレーシングの1サンプリングを計算する
    • フラグメントシェーダでパストレーシングの1サンプリングを計算する
    • 2つのFloatTextureをダブルバッファリングする
    • ダブルバッファリングで前のフレームを参照することで、サンプリング結果をFloatTextureに加算する
    • このFloatTextureはオフスクリーンレンダリングする
  2. 1.のFloatTextureをフレーム数で除算してスクリーンに描画する

ここから実装の重要な点だけ簡単に説明します。

詳細な実装についてはGitHubのリポジトリを参照していただけると幸いです。

パストレーシングの1サンプリングのフラグメントシェーダ実装

パストレーシングの1サンプリングを描画の1フレームごとにフラグメントシェーダを実装しました。

具体的には、three.jsのRawShaderMaterialを板ポリゴンに適用して、GLSLのフラグメントシェーダでパストレーシングの1サンプリングを行います。

これまでのレイマーチングレイトレーシングのフラグメントシェーダによる実装と基本は同じで、パストレーシングでも現実の光の向きとは逆に、視点→光源に光輸送経路を考えます。

違いはレイが物体表面に衝突した後のシェーディングの処理にあります。

これまでのレイトレーシングでは、物体の表面に衝突した段階でシェーディングを行いました。パストレーシングでは、物体の表面に衝突した後、BRDFに応じた方向にレイを反射させて光源を追跡することで、間接光を考慮したシェーディングを行います。

FloatTextureのダブルバッファリング

three.jsでFloatTextureのダブルバッファリングする方法を紹介します。

FloatTypeのRenderTargetの作成

three.jsではFloatTypeのRenderTargetの作成も簡単にできます。

次のコードように、THREE.WebGLRenderTargetのコンストラクタでtype: THREE.FloatTypeを指定するだけです。

function createRenderTarget( width, height ) {
    return new THREE.WebGLRenderTarget( width, height, {
        wrapS: THREE.RepeatWrapping,
        wrapT: THREE.RepeatWrapping,
        minFilter: THREE.NearestFilter,
        magFilter: THREE.NearestFilter,
        format: THREE.RGBAFormat,
        type: THREE.FloatType,// typeを指定することでFloatTypeにできる!
        stencilBuffer: false,
        depthBuffer: false
    });
}

ダブルバッファリングの実装

ダブルバッファリングの実装についても簡単に紹介します。

まず、先ほど定義したcreateRenderTarget()を用いてreadBufferwriteBufferの2つのRenderTargetを生成します。

readBuffer  = createRenderTarget( canvas.width, canvas.height );
writeBuffer = readBuffer.clone();

RenderTargetをシェーダにテクスチャとして渡す際には、.textureプロパティを使います。

floatQuad.material.uniforms.buffer.value = readBuffer.texture;

WebGLRenderer#renderの第3引数にRnderTargetを指定することでオフスクリーンレンダリングができます。普段、スクリーンにレンダリングする場合は、第3引数を省略していたわけですね。

renderer.render( floatQuad.scene, camera, writeBuffer );

描画処理の後、それぞれのRnderTargetを入れ替えることでダブルバッファリングしました。

var tmpBuffer = writeBuffer;
writeBuffer = readBuffer;
readBuffer = tmpBuffer;

実装したマテリアル

マテリアルの種類

5種類のマテリアルを実装しました。

#define MATERIAL_TYPE_DIFFUSE 0         // 完全拡散面
#define MATERIAL_TYPE_SPECULAR 1        // 完全鏡面反射
#define MATERIAL_TYPE_REFRACTION 2      // 屈折面
#define MATERIAL_TYPE_GGX 3             // GGXによるRoughnessを指定した鏡面反射
#define MATERIAL_TYPE_GGX_REFRACTION  4 // GGXによる屈折面

MATERIAL_TYPE_GGX_REFRACTIONはGGXのハーフベクトルを用いて屈折方向を計算することで、磨りガラスのようなマテリアルを表現します。

マテリアルのパラメータ

以下の5つのマテリアルに関するパラメータを指定できます。

  • Type(マテリアルの種類)
    • 前の節で説明しましたね
  • Color(色)
    • 色は光の反射率として実装しました。黒はあらゆる光を吸収し、白はあらゆる光を反射します
  • Emission(光源)
    • 光を放つ光源のマテリアルを表現するには、Emissionを指定します
    • 光源でない通常のマテリアルは #000000 を指定します。
  • Roughness(表面の粗さ)
    • GGXのRoughnessを指定します
    • MATERIAL_TYPE_GGX/MATERIAL_TYPE_GGX_REFRACTION のみ有効なパラメータです
  • Refractive Index(屈折率)
    • 屈折面の屈折率を指定します
    • MATERIAL_TYPE_REFRACTION/MATERIAL_TYPE_GGX_REFRACTION のみ有効なパラメータです

Color/Emission/Roughness はテクスチャで指定することもできます。
その場合はGUIの Color Texture/Emission Texture/Roughness Texture の項目を選択します。

最終的なEmissionの値は、インスペクタで指定した値とテクスチャの画素値を乗算した値になるで、注意が必要です。例えば、Emissionが0(#000000)の状態でテクスチャだけ変更しても、Emissionの値は0のままとなってしまいます。ですので、インスペクタ上でも適当に値を設定する必要があります。

GGX

GGXは法線分布モデルの1種です。Roughness(表面の粗さ)を指定した鏡面を表現できます。

今回のパストレーサでもGGXによる鏡面反射を実装しました。

Roughness = 0で完全鏡面反射、Roughness = 1で完全拡散反射に近い結果となります。

GGXの詳しい説明については、@_Pheema_ さんによる記事が分かりやすかったです。

重点的サンプリング

収束を高速化するために重点的サンプリングを行いました。

今回はよくある法線と入射光のなす角θのcos(θ)による重点的サンプリングを用いました。

ロシアンルーレット

今回の実装ではロシアンルーレットはせずに、定数回の反射経路のみ扱いました。

乱数

実装初期の段階ではsin()の周波数を極めて大きくした擬似乱数を用いましたが、品質に問題があったため、@kaneta1992 さんに教えていただいたdot()の乱数を最終的に利用しました。

強い光源を置いたシーンの結果を比較してみると、品質が改善したことが分かります。

sinによる乱数

図: sinによる乱数の結果

dotによる乱数

図: dotによる乱数の結果

シーンエディット機能の紹介

物体をクリックすると、緑色のワイヤーフレーム表示されたバウンディングボックスで囲まれた選択状態になります。

この状態では、次の操作ができます。

  • マウスによる、物体の位置の移動とスケールの拡大
    • 移動・拡大のモードの切り替えはScene Edit > Translate/Scaleから行えます
  • Selected Object以下のメニューからの、マテリアルのパラメータの編集
    • Remove によるオブジェクトの削除
    • Fit To Ground による地面に接地させる平行移動

また、Scene Edit > Add Sphere/Add Box からオブジェクトをシーンに追加できます。

シーンエディット機能の実装

シーンエディットの実装について簡単に紹介します。

コントローラの実装

選択状態のバウンディングボックスはthree.jsの機能によりラスタライザでレンダリングしています。

移動と拡大のコントローラはTHREE.TransformControlsを利用しました。

ラスタライザの結果とパストレーシングの結果を重ねるためにTHREE.PerspectiveCameraのカメラ行列からレイを生成しています。これにはレイマーチングとラスタライザのハイブリッドで考案した手法を利用しました。

このような平行移動や拡大できるコントローラをフルスクラッチで実装すると骨が折れますが、ラスタライザとレイトレーシングをハイブリッドで描画することで、既存のコードをうまく利用しました。

ワイヤーフレームの最前面表示

パストレーシングのシーンが常に最背面となるように、パストレーシング表示用のQuadの頂点シェーダを次のようにしました。

attribute vec3 position;
void main(void) {
    gl_Position = vec4( position.xy, 1.0, 1.0 );
}

gl_Position正規化デバイス座標のzを1とすることで、常にQuadが最背面に表示され、結果的に選択状態を示すワイヤーフレームが最前面に表示されます。

シーン情報のCPUからGPUへの橋渡し

オブジェクトの種類や位置、マテリアルのパラメータを含むシーン情報の橋渡しについては、uniformは用いずにGLSLのコードとしてフラグメントシェーダに埋め込んでいます。

つまり、オブジェクトの位置やRoughnessを編集する度にRawShaderMaterialのインスタンスを再生成し、GLSLを再コンパイルしています。かなり強引な方法ですが、実用的な速度でリアルタイムなプレビューが実現できました。

THREE.Mesh#userData の利用

THREE.Mesh#userData はユーザが任意に値を設定して良いプロパティとしてthree.jsにより提供されています。今回はマテリアルのパラメータをこれに詰めることによって、描画用のシーン情報(THREE.Scene)をそのままシーン情報のソースとして利用しました。前の節で説明したシーン情報のGLSLへのコンバートによって、three.jsの世界のシーン情報とパストレーシングの世界のシーン情報を同期しました。

このような設計にした理由は THREE.TransformControls を利用するために最適な方法と考えたためです。

THREE.Raycaster の利用

バウンディングボックスのクリックの判定にはTHREE.Raycasterを利用しました。

THREE.Raycaster#intersectObjectsTHREE.Mesh#visible = false のオブジェクトを無視する実装でした。このため、バウンディングボックスをMeshのレイヤーではなく、Materialのレイヤーで不可視にするハックが必要でした。

不可視のMaterialはコンストラクタで visible: false とすれば生成できます。

this.material = new THREE.MeshBasicMaterial( {
    color: 0x00ff00,
    wireframe: true,
    visible: false
} );

まとめと今後の課題

ブラウザ上で動作するパストレーサを three.js ( WebGL ) + GLSL で実装しました。

パストレーシングをフルスクラッチで実装した経験が無かったため、とても勉強になりました。

パストレーシングは非常に計算に時間がかかる手法ですので、工夫なしにCPUで実装すると結果の表示に数十分はかかってしまうと思います。
GPUで実装すると結果をほとんどリアルタイムに確認できるので、実装はとても楽しかったです。
GLSLによる実装は、制約が多かったりデバッグが困難だったりと大変な面がありますが、速度のメリットが大きいと改めて感じました。

今後の課題として、以下に取り組みたいです。

  • 準モンテカルロ法
  • ノイズの軽減
    • 強い光源のシーンにおけるノイズの原因の根本対応(乱数が原因?)
    • フィルタリングなどのデノイズの手法の実装
  • 対応するジオメトリの追加
    • 現状は球体と箱のジオメトリのみ対応
    • ポリゴンによるジオメトリ
      • 頂点情報をFloatTextureなどに詰めることで、ポリゴンを表示したい
      • 高速化のためのBVHなどが必要になるはずなので、Threaded BVHを実装したい
    • 距離関数によるジオメトリ
      • 当初は実装していたが、シーンエディットとの整合性のためにdeprecateしたので、復活させたい
  • 結果が正しいのか全く検証できていないので、何かしらの方法で検証したい

最後までお読みいただき、ありがとうございます。


  1. GPUからFloatTextureに書き込む処理が必要なので、OES_texture_floatWEBGL_color_buffer_floatの両方をサポートした端末でしか動作しません。ほとんどのモバイル端末はWEBGL_color_buffer_floatをサポートしていませんが、Zenfone3やKindle Fire HD 7、Nexus 5Xなどの最新のAndroidでは動作するという報告をいただいています。 

この投稿は WebGL Advent Calendar 201625日目の記事です。