Qt5でシャドウ・マッピング (Shadow Mapping)

こんにちはtatsyです。

本日はシャドウ・マッピングのQt5で実装する方法を紹介したいと思います。

シャドウ・マッピング自体の詳しい説明は省きますので、詳しく知りたい方は

などをご覧いただけると助かります。

シャドウ・マッピングとは?


最初にシャドウ・マッピングを簡単に解説したいと思います(本当にざっくりです)。

シャドウ・マッピングとは物体が別の物体に落とす影(落ち影、シャドウ)を光源から見た深度を使って計算する手法です。

通常、OpenGLで単純に描画をするだけでは立体形状に基づく陰影(シェード)は表現できても落ち影(シャドウ)を表現することはできません。

シャドウ・マッピングでは2回OpenGLの描画命令を呼び出して影を計算します。

1回目の命令では、光源からシーンを見たときの深さマップを計算します。これは、光源から見たときにシーン上で一番近い物体の深さが入ります。

2回目の命令では、実際のシーンを描画するのですが、物体表面上の各点に影が落ちるかどうかは、その点を光源から見たときの深さと、深さマップに保存されている深さを比較することで分かります。

つまり、光源から見た深さが、深さマップに保存された深さ(=光源から見たときに一番近い物体の深さ)より大きいならば、そこには影が落ちます。

Qt5でどうやるのか?


シャドウ・マッピングを実現するためにはGLSLのようなシェーダ言語を使うのがいいと思います。シェーダを使わなくても一応出来ますが結構大変だと思います。

加えてQt4でもシャドウ・マッピングはできますが、Qt5になって、ShaderやFrame Buffer周りの機能が充実してきたので、このあたりの機能を使ってやってあげようというのが今回の狙いだったりします。

具体的なやり方の手順は次のようになります。

  • QOpenGLShaderProgramでシーン描画用のシェーダと深さマップ計算用のシェーダを作る
  • QOpenGLFramebufferObjectに対して深さマップを描画し、QImageで深さマップを取得する
  • QOpenGLTextureを深さマップのQImageから作って、これを使ってシーンを描画する

以下では簡単にQOpenGLShaderProgram, QOpenGLFramebufferObject, QOpenGLTextureの使い方をご紹介します。ちなみにこれらのQOpenGLxxxx系のクラスはQt5.4以上でないと正式にサポートされないので注意してください。

QOpenGLShaderProgram

Qt5にはGLSLを使うためのQOpenGLShaderProgramというクラスが用意されています。基本的な使い方は次のようになります。

QOpenGLShaderProgram* shader = new QOpenGLShaderProgram();
shader->addShaderFromSourceFile(QOpenGLShader::Vertex, "vertex_shader.vs");
shader->addShaderFromSourceFile(QOpenGLShader::Fragment, "fragment_shader.fs");
shader->link();

もしシェーダーのコンパイルに失敗した場合にはちゃんとエラーを出してから落ちてくれるので、そんなに使うのは難しくないです。詳しい使い方は、公式のドキュメントを見ていただけると助かります。

QOpenGLFramebufferObject

シャドウ・マッピングに使う深さ情報を格納するのに使うのがフレームバッファ・オブジェクト(FBO)です。

シャドウ・マッピングを計算するには物体を光源から見たときの深さ情報が必要なのですが、この深さ情報は実際に画面に描画しません。このような描画をオフスクリーン描画と呼びますが、これを実現するのがFBOです。

Qt5にはQOpenGLFrameBufferObjectというクラスが用意されているので、これを使います。これも使い方はとても簡単で、

QOpenGLFrameBufferObject* fbo = new QOpenGLFrameBufferObject();
fbo->bind();  // 描画をするとき
fbo->release();  // 描画を終了する時

という感じになります。ちなみにシャドウ・マッピングをするときにはGL_TEXTURE_2Dに描画をするので、QOpenGLFramebufferObjectのインスタンス化は次のようにします。

QOpenGLFramebufferObject* fbo = new QOpenGLFramebufferObject(SHADOW_MAP_WIDTH, SHADOW_MAP_HEIGHT, GL_TEXTURE_2D);

QOpenGLTexture

FBOに描画された深さマップをテクスチャに直すコードが次の形になります。

toImage();
QOpenGLTexture* depthMap = new QOpenGLTexture();
depthMap->create();
depthMap->setData(img, QOpenGLTexture::MipMapGeneration::GenerateMipMaps);
// 以下フィルタとクランプのの設定
... 

なお、実際のコードではdepthMapを何度も作り直す必要があるため、そのたびにインスタンスを生成するのではなくdestroy()関数を読んで既存のテクスチャを破棄したあとで、新しいテクスチャを作る必要があります。

座標変換の注意点

シャドウ・マッピングでは、取得した深さマップをテクスチャとして使うために座標変換をする必要があります。

その変換に使う行列は、

$$ A_{bias} = \begin{pmatrix} 0.5 & 0.0 & 0.0 & 0.5 \\ 0.0 & 0.5 & 0.0 & 0.5 \\ 0.0 & 0.0 & 0.5 & 0.5 \\ 0.0 & 0.0 & 0.0 & 1.0 \end{pmatrix} $$

なのですが、glm::mat4がcolumn-majorであるのに対してQtのQMatrix4x4はrow-majorなので書き方が微妙に変わります(たぶんQtの方が直感的)。

実際には、

const QMatrix4x4 biasMat(
    0.5f, 0.0f, 0.0f, 0.5f,
    0.0f, 0.5f, 0.0f, 0.5f,
    0.0f, 0.0f, 0.5f, 0.5f,
    0.0f, 0.0f, 0.0f, 1.0f
);

と書いてあげればOKです。

その他のハマりポイント

QtではVBOに対する描画後**release()関数を読んでも勝手に描画先がディスプレイに切り替わりません。なのでシーンの描画の前にはmakeCurrent()**関数を読んで、QOpenGLWidgetに対する描画を行うことを明示してあげないといけないです(これが分からなくて3時間ぐらいハマった…)。

結果


実際にコードを実行した結果がこちらになります!

ソースコードについては、Reflective Shadow Mapsなどの発展形も含めて私のGitHubで公開しておりますので、ぜひぜひそちらも参照してみてください。

GitHub – tatsy/qt5-shadow-mapping

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