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



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

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

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

shadowmap.jpg

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

FBO生成

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
GLuint m_iFBODepth;        GLuint m_iTexDepth;        double m_fDepthSize[2];     

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);
 
        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);
 
        glGenFramebuffersEXT(1, &m_iFBODepth);
    glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, m_iFBODepth);
 
    glDrawBuffer(GL_NONE);
    glReadBuffer(GL_NONE);
 
        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でシャドウマップの解像度を設定する.解像度が高いと影の輪郭のシャギーは目立たなくなる.

シャドウマップの生成

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
struct rxFrustum
{
    double Near;
    double Far;
    double FOV;    // deg
    double W, H;
    Vec3 Origin;
    Vec3 LookAt;
    Vec3 Up;
};

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

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18

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構造体は光源設定に用いるだけでなく,カメラの設定にも用いる.

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80

void MakeShadowMap(rxFrustum &light, void (*fpDraw)(void*), void* func_obj, bool self_shading = false)
{
    glBindFramebuffer(GL_FRAMEBUFFER, m_iFBODepth);        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によりレンダリングを行う.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23

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コードを以下に示す.まず,バーテックスシェーダは,

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
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によるオブジェクトの移動・回転などだけが考慮される.

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
varying vec4 vPos;
varying vec3 vNrm;
varying vec4 vShadowCoord;
 
uniform sampler2D tex;            uniform sampler2D depth_tex;    uniform float shadow_ambient;     

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;
 
        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などを使うと影の位置がおかしくなっていた問題を修正
  • 呼び出し側の処理の簡易化

添付ファイル: fileglsl_shadowmap_v2.zip 2267件 [詳細] fileshadowmap.jpg 2520件 [詳細] fileshadowmap_result.jpg 3021件 [詳細]

トップ   編集 凍結 差分 履歴 添付 複製 名前変更 リロード   新規 一覧 検索 最終更新   ヘルプ   最終更新のRSS
Last-modified: 2022-11-30 (水) 13:48:07