----
#contents
----

*ディスプレースメントマッピング [#h56092c6]
レンダリング時にフラグメントシェーダにおいてテクスチャに格納された法線情報を参照してライティングを行うのがバンプマッピングである.
バンプマッピングでは表面の細かな凸凹を少ないポリゴン数でも表現可能であるが,
一方,実際にジオメトリが凸凹になっているわけではないので輪郭などは基のポリゴンのままである.
これに対して,テクスチャに格納されたハイトフィールドを用いて実際にジオメトリを変化させて凸凹を生成するのが
ディスプレースメントマッピング(displacement mapping)である.

*テッセレーション [#r01e7ebe]
ディスプレースメントマッピングを行う場合,まず,描画するポリゴンを細分割し,
生成された新しい頂点をテクスチャの値に応じてポリゴン法線方向に変化させるという処理を行う.
これをテッセレーション(tessellation)と呼ぶ.
テッセレーションはCPU側でやってもよいのだが,
ここではジオメトリシェーダを用いてやってみる.
ただし,前述([[GLSLによるジオメトリシェーダ#qb36625f]]参照)のようにジオメトリシェーダの最大出力頂点数の制限があるため
(各頂点が8要素を持つとして最大頂点数が128,この頂点数で三角形を生成した場合,一つの三角形の最大分割数は42となる),
CPUである程度分割した後,GPUで再度分割するという風にしてみる.

ジオメトリシェーダでテッセレーションを行うために,三角形の分割方法を検討しなければならない.
三角形ポリゴンの分割には様々な方法が考えられるが,まず,三角形を以下の図のように(s,t)でパラメータ化する(parameterize).

#ref(tri_parameterize.gif)

頂点v0を原点として,v1を(s,t)=(1,0),v2を(0,1)となるようにすると,
任意の(s,t)における座標値v(s,t)=v0+s(v1-v0)+t(v2-v0)が計算できる.

三角形の細分割方法として,ここでは元の三角形ポリゴンと同じ形状に4つに分割する方法をとる.
元の三角形ポリゴンのエッジの中点を結ぶ形で新しいポリゴンを4つ生成する.

#ref(tri_subdiv1.gif)

上の図で数値は(s,t)のパラメータ値を表す.これをさらに分割すると,

#ref(tri_subdiv2.gif)

となる.1回分割した状態をレベル1, 2回分割した状態をレベル2,...,L回分割した状態をレベルLと呼ぶ.
レベル1の時の辺の分割数は2,レベル2で4,レベルLで2^Lとなる.
また,生成される三角形の数はn=4^Lとなる.ジオメトリシェーダの最大出力要素数の制限が1024のとき,
生成できる三角形は42以下なので,レベル2が最大となる.
また,色情報がなければレベル3までは分割可能である.

上の図において赤く塗りつぶした領域に注目すると,
-s=0のエッジとs=0.25の頂点で構成される三角形が4個
-s=0の頂点とs=0.25のエッジで構成される三角形が3個

で構成されていることが分かる.緑,青の領域についても同様に考えると,
-s=i/nのエッジとs=(i+1)/nの頂点で構成される三角形が(n-i)個
-s=i/nの頂点とs=(i+1)/nのエッジで構成される三角形が(n-i-1)個

となる.ここで,i=0,1,...,n-1であり,n=4^Lである.
これをコードにすると,
#code(C){{
	int layer = 1 << level;	// 三角形の各辺の分割数

	float s0, s1, t0, t1;
	float ds = 1.0/float(layer);
	float dt = 1.0/float(layer);
	s0 = 0.0; s1 = s0+ds;
	t0 = 0.0; t1 = t0+dt;

	int n = layer;

	for(int i = 0; i < layer; ++i){	// s方向
		t0 = 0.0; t1 = t0+dt;
		for(int j = 0; j < n; ++j){	// t方向
			// (s0, t0),(s1, t0),(s0, t1)で三角形を生成
			...

			if(j != n-1){
				// (s0, t1),(s1, t0),(s1, t1)で三角形を生成
				...
			}

			t0 += dt; t1 += dt;
		}

		s0 += ds; s1 += ds;
		n--;
	}
}}
となる.


*シェーダコード [#f1c4a0b5]
頂点シェーダ,フラグメントシェーダは頂点座標や色をパススルーするだけである.
-頂点シェーダ
#code(C){{
#version 120

void main(void)
{
	gl_FrontColor = gl_Color;

	gl_Position = gl_Vertex;
	gl_TexCoord[0] = gl_TextureMatrix[0]*gl_MultiTexCoord0;
}
}}
-フラグメントシェーダ
#code(C){{
#version 120

void main(void)
{
    gl_FragColor = gl_Color;
}
}}

ジオメトリシェーダでは新しい三角形ポリゴンを生成して,テクスチャの値に応じてその座標値を変更する.

#code(C){{
#version 120
#extension GL_EXT_geometry_shader4 : enable

uniform sampler2D HeightMap;
uniform int SubLevel;
uniform float Height;

/*!
 * 三角形上のパラメータ座標値から新しい頂点を生成
 * @param[in] s,t パラメータ座標値(v0を(0,0),v1を(1.0),v2を(0,1)とする)
 */
void SetVertex(float s, float t)
{
	gl_TexCoord[0] = gl_TexCoordIn[0][0]+s*(gl_TexCoordIn[1][0]-gl_TexCoordIn[0][0])+t*(gl_TexCoordIn[2][0]-gl_TexCoordIn[0][0]);
	// gl_FrontColor = gl_FrontColorIn[0]+s*(gl_FrontColorIn[1]-gl_FrontColorIn[0])+t*(gl_FrontColorIn[2]-gl_FrontColorIn[0]);

	vec4 pos = gl_PositionIn[0]+s*(gl_PositionIn[1]-gl_PositionIn[0])+t*(gl_PositionIn[2]-gl_PositionIn[0]);
	pos.y = length(texture2D(HeightMap, gl_TexCoord[0].xy).xyz)*Height;
	gl_Position = gl_ModelViewProjectionMatrix*pos;

	EmitVertex();
}

void main(void)
{

	int level = SubLevel;
	int layer = 1 << level;	// 三角形の各辺の分割数

	// v0を(0,0),v1を(1.0),v2を(0,1)として三角形を(s,t)でパラメータ化
	float s0, s1, t0, t1;
	float ds = 1.0/float(layer);
	float dt = 1.0/float(layer);

	s0 = 0.0;
	s1 = s0+ds;

	t0 = 0.0;
	t1 = t0+dt;

	int n;
	n = layer-1;

	for(int i = 0; i < layer; ++i){	// s方向のレイヤー
		t0 = 0.0;
		t1 = t0+dt;
		for(int j = 0; j < n; ++j){	// t方向のレイヤー
			SetVertex(s0, t0);
			SetVertex(s1, t0);
			SetVertex(s0, t1);
			EndPrimitive();

			SetVertex(s0, t1);
			SetVertex(s1, t0);
			SetVertex(s1, t1);
			EndPrimitive();

			t0 += dt;
			t1 += dt;
		}

		SetVertex(s0, t0);
		SetVertex(s1, t0);
		SetVertex(s0, t1);
		EndPrimitive();

		s0 += ds;
		s1 += ds;

		n--;
	}
}
}}

元の三角形はx-z平面上にあり,高さ方向(法線方向)はy軸と平行としている.

SetVertex関数内のgl_FrontColorの行をコメントアウトしなければ新しい頂点の頂点色が線型補間で決定されるが,
生成できるポリゴン数は42に制限される.

*シェーダのビルドと呼び出し [#h849d980]
CPU側でもテッセレーションのコードを実装して,CPUである程度分割後,さらにGPUで分割して,
ディスプレースメントマッピングをしている.
CPU側のテッセレーションのコードは以下.
#code(C){{

/*!
 * 三角形上のパラメータ座標値から新しい頂点を生成して描画
 * @param[in] s,t パラメータ座標値(v0を(0,0),v1を(1.0),v2を(0,1)とする)
 * @param[in] v0,v1,v2 三角形頂点座標
 * @param[in] tc1,tc2,tc3 三角形頂点でのテクスチャ座標値
 */
void SetVertex(float s, float t, Vec3 v0, Vec3 v1, Vec3 v2, Vec2 tc0, Vec2 tc1, Vec2 tc2)
{
	Vec3 v = v0+s*(v1-v0)+t*(v2-v0);
	Vec2 tc = tc0+s*(tc1-tc0)+t*(tc2-tc0);
	glTexCoord2dv(tc.data);
	glVertex3dv(v.data);
}

/*!
 * 三角形を分割して描画
 * @param[in] v0,v1,v2 三角形頂点座標
 * @param[in] tc1,tc2,tc3 三角形頂点でのテクスチャ座標値
 * @param[in] level 分割レベル
 */
void DrawTriangle(Vec3 v0, Vec3 v1, Vec3 v2, Vec2 tc0, Vec2 tc1, Vec2 tc2, int level = 0)
{
	if(!level){
		// 分割レベル0のとき三角形をそのまま描画
		glDisable(GL_LIGHTING);
		glBegin(GL_TRIANGLES);
		glTexCoord2dv(tc0.data); glVertex3dv(v0.data);
		glTexCoord2dv(tc1.data); glVertex3dv(v1.data);
		glTexCoord2dv(tc2.data); glVertex3dv(v2.data);
		glEnd();
	}

	int layer = 1 << level;	// 三角形の各辺の分割数

	// v0を(0,0),v1を(1.0),v2を(0,1)として三角形を(s,t)でパラメータ化
	float s0, s1, t0, t1;
	float ds = 1.0/float(layer);
	float dt = 1.0/float(layer);

	s0 = 0.0;
	s1 = s0+ds;

	t0 = 0.0;
	t1 = t0+dt;

	int n;
	n = layer;

	for(int i = 0; i < layer; ++i){	// s方向のレイヤー
		t0 = 0.0;
		t1 = t0+dt;
		for(int j = 0; j < n; ++j){	// t方向のレイヤー
			glBegin(GL_TRIANGLES);
			SetVertex(s0, t0, v0, v1, v2, tc0, tc1, tc2);
			SetVertex(s1, t0, v0, v1, v2, tc0, tc1, tc2);
			SetVertex(s0, t1, v0, v1, v2, tc0, tc1, tc2);
			glEnd();

			if(j != n-1){
				glBegin(GL_TRIANGLES);
				SetVertex(s0, t1, v0, v1, v2, tc0, tc1, tc2);
				SetVertex(s1, t0, v0, v1, v2, tc0, tc1, tc2);
				SetVertex(s1, t1, v0, v1, v2, tc0, tc1, tc2);
				glEnd();
			}

			t0 += dt;
			t1 += dt;
		}

		s0 += ds;
		s1 += ds;

		n--;
	}
}
}}

GLSLの呼び出しコードは以下.
#code(C){{
/*!
 * シーンのレンダリング
 */
void RenderScene(void)
{
	glColor3f(0.0, 0.0, 1.0);

	glEnable(GL_TEXTURE_2D);
	glBindTexture(GL_TEXTURE_2D, g_uTexHeight);

	// GLSLを有効にする
	glUseProgram(g_GLSL.Prog);
	glUniform1i(glGetUniformLocation(g_GLSL.Prog, "HeightMap"), 0);
	glUniform1i(glGetUniformLocation(g_GLSL.Prog, "SubLevel"), 2);
	glUniform1f(glGetUniformLocation(g_GLSL.Prog, "Height"), 0.1);

	// ライティングは無視しているのでラインで描画
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);

	glPushMatrix();
	glTranslatef(-0.5, 0.0, -0.5);
	// 三角形ポリゴンを分割して描画
	DrawTriangle(Vec3(0.0, 0.0, 0.0), Vec3(1.0, 0.0, 0.0), Vec3(1.0, 0.0, 1.0), 
				 Vec2(0.0, 0.0), Vec2(1.0, 0.0), Vec2(1.0, 1.0), 2);
	DrawTriangle(Vec3(0.0, 0.0, 0.0), Vec3(1.0, 0.0, 1.0), Vec3(0.0, 0.0, 1.0), 
				 Vec2(0.0, 0.0), Vec2(1.0, 1.0), Vec2(0.0, 1.0), 2);
	glPopMatrix();

	glUseProgram(0);
	glBindTexture(GL_TEXTURE_2D, 0);
}
}}

g_uTexHeightにはハイトフィールドとなるテクスチャが格納されている.

*結果 [#p0150a6b]
ハイトフィールドとして以下の画像を用いた.
#ref(tex_sphere.png)
CPU側で2レベル,GPU側で2レベル,計レベル4まで分割した結果を示す.

&ref(disp_map_1_1.jpg); &ref(disp_map_2_1.jpg); 

左の画像が通常の結果,右はGLSLを無効にした場合である.
ハイトフィールド画像の暗いところほど凹んでいる.

#ref(disp_map_3_1.jpg); 
この画像はジオメトリシェーダでの分割レベルを3にした場合である.
最大出力要素数の制限により出力されていない三角形があるのが見て取れる.

*ソースコード [#e99c6dbf]
Visual Studio 2008用のソースコードを以下に置く.ビルド・実行にはGLUT,GLEW,OpenCVが必要である.
#ref(glsl_displacementmap.zip)

トップ   編集 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS