GLSLを使いシャドウマッピングで影をつける.



シャドウマッピング(Shadow mapping)について

物体の影をつける代表的なものとしてシャドウボリューム法とシャドウマッピング法がある. シャドウボリューム法は光源から各頂点へのベクトルを伸ばし, 交差によってできる三角形領域の演算処理で影領域を求める方法である. 影をつけるのには一般的にステンシルバッファが用いられる. この方法ではモデルの頂点数が増えた場合に計算量がとても多くなり, また,透過テクスチャなどの影響は考慮できない.

一方,シャドウマッピング法は光源からシーンをレンダリングし,デプスマップ(シャドウマップ)を求め, 視点からレンダリングしたときにそのデプス値を比較することで影領域を判定する. 各デプス値について下図に示す.シャドウマップ内のデプス値をdl, 視点から見たときの光源からの距離をdcとすると, 影領域ではdc > dlとなる.下図では物体自身のシェーディングにも影の影響を与えたい場合であり, シェーディングに影響しないようにしたい場合はdlを求める際に光源に対して裏面のみを描画すればよい.

shadowmap.jpg

シャドウマップを生成する際にテクスチャを考慮すれば,透過テクスチャによる影への影響も考慮可能である. ここではGLSLを使ってシャドウマッピングで影付きのレンダリングする方法について述べる.

FBO生成

光源からのレンダリング結果を格納するためにFBOを用いる. FBOについてはOpenGL - FBOを参照. FBO確保のコードは以下.

GLuint m_iFBODepth;		//!< 光源から見たときのデプスを格納するFramebuffer object
GLuint m_iTexDepth;		//!< m_iFBODepthにattachするテクスチャ
double m_fDepthSize[2];	//!< デプスを格納するテクスチャのサイズ

/*!
 * シャドウマップ用FBOの初期化
 * @param[in] w,h  シャドウマップの解像度
 */
void InitShadow(int w, int h)
{
	m_fDepthSize[0] = w;
	m_fDepthSize[1] = h;

	// デプス値テクスチャ
	glActiveTexture(GL_TEXTURE0);
	glGenTextures(1, &m_iTexDepth);
	glBindTexture(GL_TEXTURE_2D, m_iTexDepth);

	// テクスチャパラメータの設定
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

	GLfloat border_color[4] = {1, 1, 1, 1};
	glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border_color);

	// テクスチャ領域の確保(GL_DEPTH_COMPONENTを用いる)
	glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, m_fDepthSize[0], m_fDepthSize[1], 0, 
				 GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);
	glBindTexture(GL_TEXTURE_2D, 0);

	// FBO作成
	glGenFramebuffersEXT(1, &m_iFBODepth);
	glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_iFBODepth);

	glDrawBuffer(GL_NONE);
	glReadBuffer(GL_NONE);

	// デプスマップテクスチャをFBOに接続
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_iTexDepth, 0);

	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

まず,GLSLから参照するためのテクスチャ(GL_TEXTURE_2D)を作成している(15〜30行目). フォーマットがGL_DEPTH_COMPONENTになっていること以外は通常の2Dテクスチャ生成とほとんどおなじである. 34行目からFBOを作成して,テクスチャと関連づけている. 引数のw,hでシャドウマップの解像度を設定する.解像度が高いと影の輪郭のシャギーは目立たなくなる.

シャドウマップの生成

光源を視点に設定し,シャドウマップを生成する. まず,光源の広がり方などを設定するために以下の視錘台構造体を定義する.

struct rxFrustum
{
	double Near;
	double Far;
	double FOV;	// deg
	double W, H;
	Vec3 Origin;
	Vec3 LookAt;
	Vec3 Up;
};

FOV(視野角),Near,Farやアスペクト比を求めるためのW,Hの他に, 視点位置,注視点,上方向ベクトルを格納する.

rxFrustumの情報を使って視点,視錘台を設定する関数は以下.

/*!
 * プロジェクション行列,視点位置の設定
 * @param[in] f 視錘台
 */
void SetFrustum(const rxFrustum &f)
{
	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	gluPerspective(f.FOV, (double)f.W/(double)f.H, f.Near, f.Far);

	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	gluLookAt(f.Origin[0], f.Origin[1], f.Origin[2], 
			  f.LookAt[0], f.LookAt[1], f.LookAt[2], 
			  f.Up[0], f.Up[1], f.Up[2]);
	}
}

gluPerspectiveで視錘台を設定後,gluLookAtで視点を設定する. なおrxFrustum構造体は光源設定に用いるだけでなく,カメラの設定にも用いる.

光源からレンダリングして,シャドウマップを作成するコードは以下.

/*!
 * シャドウマップ(デプステクスチャ)の作成
 * @param[in] light 光源
 * @param[in] fpDraw 描画関数ポインタ
 */
void MakeShadowMap(rxFrustum &light, void (*fpDraw)(void*), void* func_obj, bool self_shading = false)
{
	glBindFramebuffer(GL_FRAMEBUFFER, m_iFBODepth);	// FBOにレンダリング
	glEnable(GL_TEXTURE_2D);	

	glUseProgram(0);

	// ビューポートをシャドウマップの大きさに変更
	glViewport(0, 0, m_fDepthSize[0], m_fDepthSize[1]);

	glClear(GL_DEPTH_BUFFER_BIT);

	// デプス値以外の色のレンダリングを無効にする
	glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); 

	double light_proj[16];
	double light_modelview[16];
	light.W = m_fDepthSize[0];
	light.H = m_fDepthSize[1];
	SetFrustum(light);

	// 光源視点のモデルビュー行列,プロジェクション行列を取得
	glMatrixMode(GL_PROJECTION);
	glGetDoublev(GL_PROJECTION_MATRIX, light_proj);

	glMatrixMode(GL_MODELVIEW);
	glGetDoublev(GL_MODELVIEW_MATRIX, light_modelview);

	glPolygonOffset(1.1f, 4.0f);
	glEnable(GL_POLYGON_OFFSET_FILL);

	glDisable(GL_LIGHTING);
	if(self_shading){
		glDisable(GL_CULL_FACE);
	}
	else{
		glEnable(GL_CULL_FACE);
		glCullFace(GL_FRONT);
	}
	fpDraw(func_obj);

	glDisable(GL_POLYGON_OFFSET_FILL);

	const double bias[16] = { 0.5, 0.0, 0.0, 0.0, 
							  0.0, 0.5, 0.0, 0.0,
							  0.0, 0.0, 0.5, 0.0,
							  0.5, 0.5, 0.5, 1.0 };

	// テクスチャモードに移行
	glMatrixMode(GL_TEXTURE);
	glActiveTexture(GL_TEXTURE7);

	glLoadIdentity();
	glLoadMatrixd(bias);

	// 光源中心座標となるようにテクスチャ行列を設定
	// テクスチャ変換行列にモデルビュー,プロジェクションを設定
	glMultMatrixd(light_proj);
	glMultMatrixd(light_modelview);
	
	// 現在のモデルビューの逆行列をかけておく
	GLfloat camera_modelview_inv[16];
	CalInvMat4x4(camera_modelview, camera_modelview_inv);
	glMultMatrixf(camera_modelview_inv);

	glMatrixMode(GL_PROJECTION);
	glLoadIdentity();
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();

	glBindFramebuffer(GL_FRAMEBUFFER, 0);

	// 無効にした色のレンダリングを有効にする
	glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); 
}

手順を以下に示す.

  1. FBOを有効にする(8行目)
  2. シャドウマップの大きさでビューポートを設定(14行目)
  3. デプスバッファを初期化(16行目)
  4. レンダリングしたいのはデプス値のみなので,glColorMaskですべての色を無効にする(19行目)
  5. 光源を視点として設定(25行目)
  6. 光源を視点とした場合のプロジェクション,モデルビュー行列を確保しておく(28-32行目)
  7. z-ファイティングを防ぐためにポリゴンオフセットを設定(34-35行目)
  8. ライティングをOFFにし,光源に対して裏の面に影をつけたくない場合はglCullFace(GL_FRONT)で表面をカリングするように設定(37-44行目)
  9. シーン描画(45行目, 引数にvoid*を設定しているのはクラスのメンバ関数を指定する際にクラスオブジェクトを渡すため)
  10. テクスチャモードに移行,GL_TEXTURE7をアクティブにする(55-56行目).ここでは他のテクスチャとなるべく競合しないようにGL_TEXTURE7を用いているが,別にテクスチャを使わないならばGL_TEXTURE0などでもよい
  11. テクスチャ行列にバイアスをかけて,クリップ空間の範囲[-1,1]をテクスチャ座標の範囲[0,1]に変換(59行目)
  12. テクスチャ行列に6で確保したプロジェクション,モデルビュー行列をかける(63-64行目)
  13. 頂点シェーダで座標にモデルビュー行列をかけるが,オブジェクトローカル座標内での変換のみを適用したいので,現在のモデルビュー行列の逆行列をかける(67-69行目).CalInvMat4x4関数は4x4行列の逆行列を計算する(4x4までの逆行列の直接解法参照).
  14. プロジェクション,モデルビュー行列をリセット(71-74行目).glPushMatrix,glPopMatrixでMakeShadowMap関数を実行する前の状態に戻しても良いが,面倒なのでリセットして呼び出し側で再設定するようにしている.
  15. FBOを無効にする(76行目)
  16. 4で無効にした色のレンダリングを有効にする(79行目)

この関数を実行すると,シャドウマップがFBOに関連づけられたテクスチャに格納され, テクスチャ行列には光源を視点とする変換が格納される.

シャドウマップを考慮してレンダリング

作成したシャドウマップをバインドしてシーンをレンダリングする. このとき,GLSLによりレンダリングを行う.

/*!
 * 影付きでシーン描画
 * @param[in] camera 視点
 * @param[in] fpDraw 描画関数のポインタ
 */
void RenderSceneWithShadow(rxFrustum &camera, void (*fpDraw)(void*), void* func_obj)
{
	// 視点設定
	SetFrustum(camera);

	glEnable(GL_TEXTURE_2D);

	// デプステクスチャを貼り付け
	glActiveTexture(GL_TEXTURE7);
	glBindTexture(GL_TEXTURE_2D, m_iTexDepth);
	
	glEnable(GL_CULL_FACE);
	glCullFace(GL_BACK);
	fpDraw(func_obj);

	glBindTexture(GL_TEXTURE_2D, 0);

}

SetFrustumで視点を設定し,シャドウマップテクスチャを貼り付けてシーンを描画する. この描画に用いるGLSLコードを以下に示す.まず,バーテックスシェーダは,

// フラグメントシェーダに値を渡すための変数
varying vec4 vPos;
varying vec3 vNrm;
varying vec4 vShadowCoord;	//!< シャドウデプスマップの参照用座標

void main(void)
{
	// フラグメントシェーダでの計算用(モデルビュー変換のみ)
	vPos = gl_ModelViewMatrix*gl_Vertex;			// 頂点位置
	vNrm = normalize(gl_NormalMatrix*gl_Normal);	// 頂点法線
	vShadowCoord = gl_TextureMatrix[7]*gl_ModelViewMatrix*gl_Vertex;	// 影用座標値(光源中心座標)

	// 描画用
	gl_Position = gl_ProjectionMatrix*vPos;	// 頂点位置
	gl_FrontColor = gl_Color;				// 頂点色
	gl_TexCoord[0] = gl_MultiTexCoord0;		// 頂点テクスチャ座標
}

vPosとvNrmはフォンシェーディングに用いるものでシャドウマッピングには直接関係しない. 11行目で頂点位置(gl_Vertex)にGL_TEXTURE7のテクスチャ行列(gl_TextureMatrix[7])とモデルビュー変換行列をかけて, 頂点を光源座標系に変換し,vShadowCoordに格納,フラグメントシェーダに渡している. 注意として,gl_TextureMatrix[7]には視点移動などのためのモデルビュー変換の逆行列がすでにかかっているので, ここでは単純にglTranslateやglRotateによるオブジェクトの移動・回転などだけが考慮される.

フラグメントシェーダは以下.

// バーテックスシェーダから受け取る変数
varying vec4 vPos;
varying vec3 vNrm;
varying vec4 vShadowCoord;

// GLから設定される定数(uniform)
uniform sampler2D tex;			//!< 模様
uniform sampler2D depth_tex;	//!< デプス値テクスチャ
uniform float shadow_ambient;	//!< 影の濃さ

/*!
 * 影生成のための係数(影のあるところで1, それ以外で0)
 * @return 影係数(影のあるところで1, それ以外で0)
 */
float ShadowCoef(void)
{
	// 光源座標
	vec4 shadow_coord1 = vShadowCoord/vShadowCoord.w;

	// 光源からのデプス値(視点)
	float view_d = shadow_coord1.z;//-0.0001;
	
	// 格納された光源からの最小デプス値を取得
	float light_d = texture2D(depth_tex, shadow_coord1.xy).z;

	// 影で0,日向で1
	float shadow_coef = 1.0;
	if(vShadowCoord.w > 0.0){
		shadow_coef = light_d < view_d ? 0.0 : 1.0;
	}

	return shadow_coef;
}

void main(void)
{	
	// 表面反射色
	vec4 light_col = PhongShading();

	// 影影響係数
	float shadow_coef = ShadowCoef();

	// 出力
	gl_FragColor = shadow_ambient*shadow_coef*light_col+(1.0-shadow_ambient)*light_col;
}

ShadowCoef関数(15-33行目)で影の影響を計算する. まず,wで割ることで光源座標値を計算する(18行目). 頂点シェーダでこの処理を行うと不正確な値となるので注意 (フラグメントシェーダに渡されるときに補間された値を使って割る). 光源座標値のzがその位置における光源からの距離となる(21行目). シャドウマップを生成するときにglCullFace(GL_FRONT)を使わなかった場合, view_d == light_dの場所でstitchingが発生する.コメントアウトしている-0.0001はそれを防ぐためのもの.

次に,シャドウマップを参照して光源からみたときの最小デプス値を取得する(24行目). この値を光源からの距離と比較することで影で0,日向で1となるような係数を算出する(29行目).

main関数(35-45行目)ではGLSLによるフォンシェーディングで解説したフォンシェーディングで 表面色を算出(38行目),ShadowCoef関数で求めた係数をかけることで最終的な色を出力している(45行目).

実行結果

実行結果のスクリーンショットを以下に示す(クリックで拡大).

shadowmap_result.jpg

左下のはシャドウマップ.

ソースコード

Visual Studio 2010用のソースコードを以下に置く(要GLUT,GLEW).

Ver2(2013.5.22更新)

  • オブジェクト描画側でglTranslateなどを使うと影の位置がおかしくなっていた問題を修正
  • 呼び出し側の処理の簡易化

添付ファイル: fileshadowmap.jpg 2617件 [詳細] fileglsl_shadowmap_v2.zip 2356件 [詳細] fileshadowmap_result.jpg 3117件 [詳細]

トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2024-03-08 (金) 18:06:04