OpenGLでDisplacement mapping

こんにちはtatsyです。

今回はOpenGLのTesselation control shaderとTesselation evaluation shaderを使ってDisplacement mappingをしてみたいと思います。

例のごとく、Displacement mappingの説明から始めますが、結果のみをご覧になりたい方は、お手数ですが下までスクロールをお願いいたします。

Displacement mappingとは?


Displacement mappingは端的に言うと、テクスチャにより与えられた高さマップを描画時に評価して、描画対象の物体に凹凸をつける手法です。

類似した手法には法線マップを使うBump mappingがありますが、こちらは実際に描画される形状は変化させず、法線のズレだけを使ってシェーディングの仕方を変えることで、擬似的に凹凸を表現する手法です。

これらの手法の違いについては、さまざまなページで解説がなされていますので、そちらを参照してくだされば幸いです (図を作るのが面倒だった…)。

テクスチャマップの理解 | CG WORLD – http://cgworld.jp/feature/1602-3dtotal-digitaltutors-2.html

Displacement mappingを実現するには?


Displacement mappingにより凹凸を作り出すためには、通常、面の法線方向とテクスチャで与えられた高さ場を使い、頂点位置を法線方向に動かします。

ですので、単にDisplacement mappingを利用するだけであれば、ジオメトリシェーダだけ使えば実現できそうです。

すなわち、ジオメトリシェーダで頂点をフラグメントシェーダに送る前に法線方向の定数倍を頂点に足してあげればいいです。

ですが、ここで1点問題となるのは、テクスチャで与えられる高さ場が、ものすごく細かいテクスチャである場合です。

今回、実験で使うテクスチャは、以下のようなレンガ模様なのですが、このようなテクスチャの場合には、メッシュが十分に細かくないとレンガ模様の凹凸がうまく表現できません。

じゃあ、メッシュを予め細かくしておけばいいのかというと、そこはなかなか難しくて、メッシュを細かくすればデータ量が増えますので、ディスクに入れられるメッシュモデルの容量に限界がある場合やロード時間を短縮したい場合などには、細かくすることは避けたい事項です。

そこで登場するのがTessellation control shaderおよびTessellation evaluation shaderです。

これらは、OpenGL 4.0から追加されたシェーダで、GPU上で三角形を細かくする処理(=テッセレーション)を実施してくれるシェーダです。

Tesselation control/evaluation shaderを使う準備


正直あんまり準備は必要ありませんが、2点ほど気をつけなければならない点があります。

1点目は描画命令の呼び出し方が変わるという点で、通常、三角形を描画するときに

glDrawElements(GL_TRIANGLES, ...);

のように書いていた部分が、

glDrawElements(GL_PATCHES, ...);

のように変わります。これをしないと画面に何も出なくなります(これで数時間悩んだ…)。

2点目の注意点は、描画されるパッチが持つ頂点の数を指定してあげることです。

前述の通り、描画される対象は GL_PATHCES で表される通りパッチです。このパッチがいくつの頂点を持つのかを glPatchParameteri 関数で指定します。

設定先の変数は GL_PATCH_VERTICES で初期値は3です。ですので、三角形の場合には特に指定する必要はありませんが、念のため、

glPatchParameteri(GL_PATCH_VERTICES, 3);

のように書いておきます。もし四角形を描画する場合には第2引数が4になります。

テッセレーションに使うパラメータで重要なものとして、これ以外に GL_PATCH_DEFAULT_INNER_LEVEL および GL_PATCH_DEFAULT_OUTER_LEVEL がありますが、こちらはシェーダ内でも指定ができますので、今回はシェーダの中で記述することにします。

Tessellation control/evalution shaderの記述


Tesselation control shaderおよびTessellation evaluation shaderでは言わずもがな、テッセレーションのやり方を指定します。

このときTessellation control shaderでは、テッセレーションをどのくらいの細かさで行うのかを指定します。

するとその指定された値をもとにGPU上でテッセレーションがなされて、その結果がTessellation evaluation shaderに送られます。

では、Tessellation evaluation shaderは何をするかというと、受け取った三角形(あるいは線分、四角形)の各頂点に与えられるべき法線やテクスチャ座標などの情報を計算します。Tessellation evaluation shaderの中では、もともとの三角形上のどのあたりに対応する部分三角形なのか、という情報が使える点がジオメトリシェーダと異なる点です。

Tessellation evaluation shaderから出力された頂点情報は、次にジオメトリシェーダに渡されます。直接フラグメントシェーダにも渡せるのかな、と思い実験してみたのですが、その場合には何も表示されなくなったので、コードが間違っているか、あるいは、直接フラグメントシェーダには出力できないのかもしれません(ちなみにシェーダのリンクエラーにはならない)。

では、早速、各シェーダで何をやっているかを見ていきます。

Tessellation control shader

#version 410

layout(vertices = 3) out;

layout(location = 0) in vec3 tescPosition[];
layout(location = 1) in vec3 tescNormal[];
layout(location = 2) in vec2 tescTexCoord[];

layout(location = 0) out vec3 tesePosition[];
layout(location = 1) out vec3 teseNormal[];
layout(location = 2) out vec2 teseTexCoord[];

void main(void) {
    tesePosition[gl_InvocationID] = tescPosition[gl_InvocationID];
    teseNormal[gl_InvocationID] = tescNormal[gl_InvocationID];
    teseTexCoord[gl_InvocationID] = tescTexCoord[gl_InvocationID];

    gl_TessLevelInner[0] = 20.0;
    gl_TessLevelOuter[0] = 10.0;
    gl_TessLevelOuter[1] = 10.0;
    gl_TessLevelOuter[2] = 10.0;
}

さて、こちらのコードですが、入力は頂点シェーダから受け取った頂点位置、法線、テクスチャ座標の情報です。これらをそのままTessellation evaluation shaderに送ります。

ですが、コードの下側では gl_TessLevelInner および gl_TessLevelOuter に何らかの値を代入しています。

これらの変数は名前の通り、三角形の内側と外側をどのくらい細かく分割するかを表す変数です。

三角形の場合にはinner levelに1つの変数を、outer levelに3つの変数を指定できます。

まず、innerの方ですが、こちらは三角形内部を入力の三角形と相似な小三角形で分離します。この時、inner levelはある辺から中央を通って別の辺に抜ける間に通る領域の数を表します。

(OpenGL Wikiより引用)

一方で、outerの方は3つの変数が指定できますが、これは三角形の各辺がいくつに分解されるかを指定するものです。ですので、上の図であれば、各辺が3つずつに分解されているので、3つの変数は全て3です。

このようにして分割頂点が作られたら、あとはそれらを良しなに結んで三角形を作ってくれます。

もし四角形を分割する場合にはinnerはU方向とV方向で2種類、outerの方は4つの辺それぞれに対して1つずつの合計4つを指定できます。

Tessellation evaluation shader

#version 410

layout(triangles, equal_spacing, ccw) in;

layout(location = 0) in vec3 tesePosition[];
layout(location = 1) in vec3 teseNormal[];
layout(location = 2) in vec2 teseTexCoord[];

layout(location = 0) out vec3 geoPosition;
layout(location = 2) out vec2 geoTexCoord;

uniform sampler2D u_texture;

void main(void) {
    vec3 p0 = gl_TessCoord.x * tesePosition[0];
    vec3 p1 = gl_TessCoord.y * tesePosition[1];
    vec3 p2 = gl_TessCoord.z * tesePosition[2];
    vec3 position = p0 + p1 + p2;

    vec3 n0 = gl_TessCoord.x * teseNormal[0];
    vec3 n1 = gl_TessCoord.y * teseNormal[1];
    vec3 n2 = gl_TessCoord.z * teseNormal[2];
    vec3 normal = n0 + n1 + n2;

    vec2 tc0 = gl_TessCoord.x * teseTexCoord[0];
    vec2 tc1 = gl_TessCoord.y * teseTexCoord[1];
    vec2 tc2 = gl_TessCoord.z * teseTexCoord[2];
    geoTexCoord = tc0 + tc1 + tc2;

    vec3 rgb = texture(u_texture, geoTexCoord).xyz;
    float height = (0.30 * rgb.r + 0.59 * rgb.g + 0.11 * rgb.b);
    position += normal * height * 0.05;
    geoPosition = position;
}

こちらのコードは三角形を分割したあとの分割頂点の重心座標である “`gl_TessCoord“ を使って、分割後の頂点情報を求めています。こちらは単なる線形補間によって頂点位置、法線、テクスチャ座標を求めております。

肝心のDisplacement mappingに対応する部分はテクスチャから色のRGB値を取得、それをグレースケールに変換して、その大きさの分だけ法線方向に頂点位置を動かしています。

Fragment shader

#version 410

layout(location = 0) in vec3 posWorldSpace;
layout(location = 2) in vec2 texCoord;

out vec4 outColor;

uniform mat4 u_viewMat;
uniform mat4 u_modelMat;
uniform vec3 u_lightPos;
uniform sampler2D u_texture;

void main(void) {
    mat4 mvMat = u_modelMat * u_viewMat;
    mat4 normMat = inverse(transpose(mvMat));
    vec3 normWorldSpace = normalize(cross(dFdx(posWorldSpace), dFdy(posWorldSpace)));

    vec3 posViewSpace = (mvMat * vec4(posWorldSpace, 1.0)).xyz;
    vec3 normViewSpace = (normMat * vec4(normWorldSpace, 0.0)).xyz;
    vec3 lightPosViewSpace = (mvMat * vec4(u_lightPos, 1.0)).xyz;

    vec3 V = normalize(-posViewSpace);
    vec3 N = normalize(normViewSpace);
    vec3 L = normalize(lightPosViewSpace - posViewSpace);
    vec3 H = normalize(V + L);

    float ndotl = max(0.0, dot(N, L));
    float ndoth = max(0.0, dot(N, H));

    vec3 diffColor = texture(u_texture, texCoord).rgb;
    vec3 specColor = vec3(0.5, 0.5, 0.5);

    outColor.rgb = ndotl * diffColor + pow(ndoth, 128.0) * specColor;
    outColor.a = 1.0;
}

ジオメトリシェーダは単に受け取った値をフラグメントシェーダに送っているだけなので、最後はフラグメントシェーダの中身を説明します。

こちらのコードは基本はBlinn-Phongのシェーディングモデルを実行しているだけですが、注意点が一つあります。

それは、シェーディングに用いる法線の計算です。

各頂点における法線はここでは微分情報を使って再計算されています。というのも、ジオメトリシェーダ等で、三角形を構成する頂点の情報から法線を計算すると、それは面ごとの法線になってしまい、スムーズなシェーディングにならないからです。

描画結果


こちらに今回のコードを実行した結果を動画で示します。

きちんとDisplaceしているのですが、この動画だと若干分かりづらいですかね。あんまりやりすぎると今度はレンガとして不自然になってくるので、このくらいの凹凸になっています。

まとめ


というわけで、今回はOpenGLを使ってDisplacement mappingをやる方法を説明させていただきました。

今回ご紹介したプログラムのソースコードは私のGitHubからダウンロード可能です。

https://github.com/tatsy/QtOpenGLTutorials/tree/0e6074b5f046809991989167e3d8d5213e14faaf/sources/displacement_mapping

それでは、今回も最後までお読みいただきありがとうございました。