■ 2006年05月25日 [OpenGL][GLSL] 第8回 視差マッピング
輪講の当番
輪講の当番を学生さん自身に決めてもらったら,その中になぜか私も組み込まれていたので,GPU Gems 2 の Chapter 8: Per-Pixel Displacement Mapping with Distance Functions を取り上げてみました.これは Displacement Mapping を高速化するために 3D テクスチャ (Distance Map) を使うという,結構男っぽい手法です.これを自分で実装してみる気力は今の私には微塵も無いので,とりあえずこの論文の Previous Work として引用されている視差マッピング (Parallax Mapping) を試してみることにしました.視差マッピングは t-pot さんも紹介されていますが,ここでは GLSL による実装を考えてみます(ったって t-pot さんのと変わらんのだけども).
バンプマッピングの問題点
いま,次のようなへこみのある四角形を描くことを考えます.
このへこみをバンプマッピングで表現しているとき,この四角形を斜めから平行投影すると,次のような表示が得られます.
しかし,このへこみが実際にへこんだ形をしていれば,この四角形は次のように表示されるはずです.
つまり実際にへこんだ形状では,視点からの距離がへこんだ部分と周囲とで異なるために,斜めから見たときにこのようなずれが生じます.ところがバンプマッピングでは,陰影の変化のみによってへこみの表現を行っているために,このずれを表現することができません.視差マッピングではこのずれを再現することによって,よりリアルな凹凸を表現することができます.
視差マッピングはもともとバーチャルリアリティの研究において開発された手法のようです.バーチャルリアリティにおいて両眼視差による立体視表示を行う際,バンプマッピングでは凹凸に視差の影響が反映されないために,凹凸の立体感が消失してしまいます.視差マッピングはバンプマッピングに視差の影響を加味することによって,この問題を解消することができます.
ディフューズテクスチャを追加する
雛型にはGLSL によるバンプマッピングで作成したプログラムを使います.まず最初に,これにディフューズテクスチャ(拡散反射光成分のテクスチャ)のマッピングを追加します.テクスチャには,以前に使ったこの画像を使います.
マルチテクスチャを使うので,Windows では glActiveTexture() が使えるようにしておきます.また,テクスチャオブジェクト用の変数も用意しておきます.main.cpp を次のように変更します.
#include <stdio.h> #include <stdlib.h> #include <math.h> #if defined(WIN32) //# pragma comment(linker, "/subsystem:\"windows\" /entry:\"mainCRTStartup\"") # include "glut.h" # include "glext.h" PFNGLACTIVETEXTUREPROC glActiveTexture; #elif defined(__APPLE__) || defined(MACOSX) # include <GLUT/glut.h> #else # define GL_GLEXT_PROTOTYPES # include <GL/glut.h> #endif ・・・ /* ** 初期化 */ static void init(void) { /* 法線マップを格納する配列 */ GLubyte texture[TEXHEIGHT * TEXWIDTH * 4]; /* シェーダプログラムのコンパイル/リンク結果を得る変数 */ GLint compiled, linked; /* テクスチャオブジェクト */ GLuint texname[1]; /* テクスチャファイルの読み込みに使うファイルポインタ */ FILE *fp; #if defined(WIN32) glActiveTexture = (PFNGLACTIVETEXTUREPROC)wglGetProcAddress("glActiveTexture"); #endif ・・・
そしてテクスチャユニット1にディフューズテクスチャを割り当てます.
・・・ /* テクスチャユニット0に法線マップを割り当てる */ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture); /* テクスチャユニット1用のテクスチャオブジェクトを作成する */ glGenTextures(1, texname); /* テクスチャユニット1に切り替える */ glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, texname[0]); /* テクスチャを拡大・縮小する方法の指定 */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); /* テクスチャの繰り返し方法の指定 */ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); /* テクスチャ画像の読み込み */ if ((fp = fopen("dot.raw", "rb")) != NULL) { fread(texture, sizeof texture, 1, fp); fclose(fp); } /* テクスチャの割り当て */ glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, TEXWIDTH, TEXHEIGHT, 0, GL_RGBA, GL_UNSIGNED_BYTE, texture); /* テクスチャユニット0に戻す */ glActiveTexture(GL_TEXTURE0); /* 初期設定 */ glClearColor(0.3, 0.3, 1.0, 0.0); glEnable(GL_DEPTH_TEST); glEnable(GL_CULL_FACE); ・・・
最後に,ディフューズテクスチャを割り当てたテクスチャユニットの番号を,シェーダプログラムで使う uniform 変数に設定します.
・・・ /* シェーダプログラムの適用 */ glUseProgram(gl2Program); /* テクスチャユニット0を指定する */ glUniform1i(glGetUniformLocation(gl2Program, "texture"), 0); /* テクスチャユニット1を指定する */ glUniform1i(glGetUniformLocation(gl2Program, "dtexture"), 1); /* 接線ベクトルを渡すために使う attribute 変数のハンドルを得る */ tangent = glGetAttribLocation(gl2Program, "tangent"); } ・・・
あと,一応テクスチャをマッピングする部分で,テクスチャマッピングを有効にしておきます.「一応」というのは,もしかしたら GLSL ではこの glEnable(GL_TEXTURE_2D) は不要なのではないかと思うからです.マッピングするかしないかはシェーダプログラムで決めることができますし,実際,無くても動くので…
・・・ /* ** シーンの描画 */ static void scene(void) { static const GLfloat diffuse[] = { 0.6, 0.1, 0.1, 1.0 }; static const GLfloat specular[] = { 0.3, 0.3, 0.3, 1.0 }; /* 材質の設定 */ glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, diffuse); glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, specular); glMaterialf(GL_FRONT_AND_BACK, GL_SHININESS, 100.0f); /* 法線マップのマッピング開始 */ glEnable(GL_TEXTURE_2D); /* ディフューズテクスチャのマッピング開始 */ glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); /* トラックボール処理による回転 */ glMultMatrixd(trackballRotation()); /* 球を描く */ sphere(1.0, 64, 32); /* ディフューズテクスチャのマッピング終了 */ glEnable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); /* 法線マップのマッピング終了 */ glDisable(GL_TEXTURE_2D); } ・・・
フラグメントシェーダプログラム bump.frag は次のように変更します.まず,ディフューズテクスチャを割り当てたテクスチャユニットを指定する uniform 変数 dtexture を宣言します.そして,そのテクスチャユニット使ってテクスチャをサンプリングして得た色 dcolor を使って陰影付けを行います.
// bump.frag uniform sampler2D texture; uniform sampler2D dtexture; varying vec3 light; varying vec3 view; void main (void) { vec4 color = texture2DProj(texture, gl_TexCoord[0]); vec3 fnormal = vec3(color) * 2.0 - 1.0; vec3 flight = normalize(light); float diffuse = dot(flight, fnormal); vec4 dcolor = texture2DProj(dtexture, gl_TexCoord[0]); gl_FragColor = dcolor * gl_LightSource[0].ambient; if (diffuse > 0.0) { vec3 fview = normalize(view); vec3 halfway = normalize(flight - fview); float specular = pow(max(dot(fnormal, halfway), 0.0), gl_FrontMaterial.shininess); gl_FragColor += dcolor * gl_LightSource[0].diffuse * diffuse + gl_FrontLightProduct[0].specular * specular; } }
これでディフューズテクスチャがマッピングされます.
高さマップの読み込み
視差マッピングではバンプの高さを考慮するので,法線マップのほかに高さマップも読み込んでおく必要があります.ここでは法線マップを作成する際,そのアルファチャンネルに高さマップを入れておくことにします.normalmap.cpp を次のように変更します.
・・・ /* ** 高さマップをもとに法線マップを作成する */ void makeNormalMap(GLubyte *tex, int width, int height, double nz, const char *name) { FILE *fp = fopen(name, "rb"); if (fp) { unsigned char *map = (unsigned char *)malloc(width * height); if (map) { unsigned long size = width * height; /* 高さマップを読み込む */ fread(map, height, width, fp); fclose(fp); for (unsigned long y = 0; y < size; y += width) { for (int x = 0; x < width; ++x) { /* 隣接する画素との値の差を法線ベクトルの成分に用いる */ double nx = map[y + x] - map[y + (x + 1) % width]; double ny = map[y + x] - map[(y + width) % size + x]; /* 法線ベクトルの長さを求めておく */ double nl = sqrt(nx * nx + ny * ny + nz * nz); *(tex++) = (GLubyte)(nx * 127.5 / nl + 127.5); *(tex++) = (GLubyte)(ny * 127.5 / nl + 127.5); *(tex++) = (GLubyte)(nz * 127.5 / nl + 127.5); *(tex++) = map[y + x]; } } free(map); } } }
視差マッピングの実装
ようやくここから本題です.物体表面に高さマップを重ねたと考えると,それによる注視点位置のずれは下図の様になります.ただし,これは高さマップが十分なだらかであると仮定した場合の近似です.
したがってフラグメントシェーダ内で高さマップを参照し,それに視線の方向ベクトルを掛けて,テクスチャ座標をずらす量を決定します.そのために,先に視線の方向単位ベクトル fview を求めます.
// bump.frag uniform sampler2D texture; uniform sampler2D dtexture; varying vec3 light; varying vec3 view; void main (void) { vec4 color = texture2DProj(texture, gl_TexCoord[0]); vec3 fview = normalize(view);
視線の方向ベクトルの xy 成分に法線マップのアルファチャンネルに入れておいた高さマップの値を乗じます.これをテクスチャ座標から引いて,ずらしたテクスチャ座標 texcoord を求めます.係数の 0.02 はバンプの実際の高さの最高値のようなもので,この値が大きいほどバンプが強くなります.しかし,バンプ内での隠面消去処理を行っていないので,あまり大きな値を設定すると不自然な表示になります.
vec2 texcoord = gl_TexCoord[0].xy - fview.xy * color.a * 0.02;
ずらしたテクスチャ座標を使って法線マップをサンプリングし,法線ベクトル fnormal を求めます.また,ディフューズテクスチャもこのテクスチャ座標を使ってサンプリングします.
vec3 fnormal = vec3(texture2D(texture, texcoord)) * 2.0 - 1.0; vec3 flight = normalize(light); float diffuse = dot(flight, fnormal); vec4 dcolor = texture2D(dtexture, texcoord); gl_FragColor = dcolor * gl_LightSource[0].ambient; if (diffuse > 0.0) { //vec3 fview = normalize(view); (削除) vec3 halfway = normalize(flight - fview); float specular = pow(max(dot(fnormal, halfway), 0.0), gl_FrontMaterial.shininess); gl_FragColor += dcolor * gl_LightSource[0].diffuse * diffuse + gl_FrontLightProduct[0].specular * specular; } }
バンプマッピングと視差マッピングを見比べてみます.左がバンプマッピングで,右が視差マッピングです.視差マッピングのほうがバンプが強く出るというか,全体的に盛り上がったような感じになっています.また視点の移動によって盛り上がり部分のずれが変化するため,実際にへこみがあるように感じられます(よくわからないという人は,前述の係数 0.02 を 0.05 くらいにしてみてください).
視差マッピングの問題点
視差マッピングの問題点は,前述のとおりバンプ自体の隠面消去処理を行っていないために,バンプを強くしたり浅い角度から眺めたりしたときに,膨らんだところに隠されて見えないはずの部分が見えてしまうところにあります.また,高さマップが急激に変化するような場合は,得られる注視点のずれの誤差が大きくなってしまうという問題もあります.
これらを解消するには,視線が高さマップと交差する位置を正確に求める,いわゆるディスプレースメントマッピングを用いる必要があります.ただ,実際にこれを実現しているレリーフマッピングや輪講で取り上げた Per-Pixel Displacement Mapping with Distance Functions は,いずれも視線と高さマップの交点をもとめるためにレイトレーシング的な手法を用いています.
なおレリーフマッピングでは,多層の高さマップを用いて近似的な交点を高速に求める手法が提案されています.また Per-Pixel Displacement Mapping with Distance Functions は,高さマップからの距離を 3D テクスチャに格納した Distance Map に対して Sphere Tracing というレイトレーシングの高速化手法を実行することによって,交点の算出を効率化しています.