OpenGL超級寶典筆記——深度紋理和陰影

以前咱們介紹過簡單的把物體壓平到投影平面來製造陰影。但這種陰影方式有其侷限性(如投影平面須是平面)。在OpenGL1.4引入了一種新的方法陰影貼圖來產生陰影。html

陰影貼圖背後的原理是簡單的。咱們先把光源的位置看成照相機的位置,咱們從這個位置觀察物體,咱們就知道哪些物體的表面是被照射到(被光源看到)的,哪些是沒有被照射到(被遮擋住)的(在某個方向上離光源最近的表面是被照射的,後面的表面則沒有被照射到)。咱們開啓深度測試,這樣咱們就能夠獲得一個有用的深度緩衝區數據(每個像素在深度緩衝區中的結果),而後咱們從深度緩衝區中讀取數據做爲一個陰影紋理,投影回場景中,而後咱們在使用照相機的視角,來渲染物體。git

image

光源視角

首先咱們把視角移到光源的位置。咱們能夠經過glu庫的輔助函數:github

gluLookAt(lightPos[0], lightPos[1], lightPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);app

把光源的位置設置爲觀察的位置。函數

爲了以最佳的方式利用空間來產生陰影貼圖。從光源的角度看過去的透視可視區域要適應窗口的比例,且透視的最近平面位置是裏光源最近的物體的平面,最遠的平面位置是離光源最遠的物體的平面。這樣咱們就能夠充分的利用場景的信息來填充深度緩衝區,來製造陰影貼圖。咱們估計剛好包好整個場景的視野。測試

//場景的半徑大小
GLfloat sceneBoundingRadius = 95.0f;

//光的距離
lightToSceneDistance = sqrt(lightPos[0] * lightPos[0] +
												lightPos[1] * lightPos[1] +
												lightPos[2] * lightPos[2]);

//近裁剪平面
nearPlane = lightToSceneDistance - sceneBoundingRadius;
//讓場景充滿整個深度紋理
fieldOfView = (GLfloat)m3dRadToDeg(2.0f * atan(sceneBoundingRadius/lightToSceneDistance));

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(fieldOfView, 1.0f, nearPlane, nearPlane + (2.0f * sceneBoundingRadius));

在上面的代碼中,場景的中心位於原點,場景中全部的物體,在以原點爲中心,半徑爲sceneBoundingRadius的圓中。這是咱們對場景的粗略估計。大體以下圖:spa

image

由於咱們只須要獲得像素通過深度測試後,深度緩衝區的結果。因此咱們能夠去掉一切沒必要要的的細節,不往顏色緩衝區中寫數據由於不須要顯示。.net

glShadeModel(GL_FLAT);
glDisable(GL_LIGHTING);
glDisable(GL_COLOR_MATERIAL);
glDisable(GL_NORMALIZE);
glColorMask(0,0,0,0);
...

若是咱們能夠看到深度緩衝區,深度緩衝區的灰度圖大概是這樣子的。3d

image

新型的紋理

咱們須要拷貝深度的數據到紋理中做爲陰影貼圖。在OpenGL1.4以後,glCopyTexImage2D容許咱們從深度緩衝區中拷貝數據。紋理數據多了一種深度紋理的類型,其內部格式包括GL_DEPTH_COMPONENT16,GL_DEPTH_COMPONENT24,GL_DEPTH_COMPONENT32,數字表明每一個紋理單元包含的位數。通常狀況下,咱們但願其內部格式與深度緩衝區的精度相匹配。OpenGL容許你指定通用的GL_DEPTH_COMPONENT格式來匹配你的深度緩衝區。在以光源的視角繪製後,咱們把深度緩衝區的數據拷貝出來做爲深度紋理:code

glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 0, 0, shadowWidth, shadowHeight, 0);

只有在物體移動或者光源移動時,才須要從新產生深度紋理。若是僅僅是照相機移動,咱們並不須要從新產生深度紋理,由於以光源的角度來看,深度紋理沒有變化。當窗口的大小改變時,咱們也須要產生一個更大的深度紋理。

深度紋理的大小

在OpenGL2.0以前,在不支持非二次冪的紋理(GL_ARB_texture_non_power_of_two)的擴展的狀況下,咱們須要調整深度紋理的大小,使其剛好爲二次冪。例如在1024x768的分辨率下,最大的二次冪紋理大小是1024x512.

void ChangeSize(int w, int h)
{
	windowWidth = shadowWidth = w;
	windowHeight = shadowHeight  = h;
	//不支持非二次冪紋理大小
	if(!nptTextureAvailable)
	{
		int i = 0;
		int j = 0;
		//得到二次冪的寬度
		while((1 << i) <= shadowWidth )
			i++;

		shadowWidth = (1 << (i-1));
		//二次冪的高度
		while((1 << j) <= shadowHeight )
			j++;

		shadowHeight = (1 << (j-1));
	}

}

首先繪製陰影

若是陰影被定義爲徹底沒有光照的,那麼咱們不須要繪製它。例如只有單一的聚光燈做爲光源,那讓陰影是全黑色的就足以知足咱們的要求了。若是咱們不但願陰影是全黑的,並且須要陰影區域中的一些細節,那麼咱們須要在場景中模擬一些環境光。同時,咱們還添加一些散射光,幫助傳遞形狀的信息。

GLfloat lowAmbient[4] = {0.1f, 0.1f, 0.1f, 1.0f};
GLfloat lowDiffuse[4] = {0.35f, 0.35f, 0.35f, 1.0f};

glLightfv(GL_LIGHT0, GL_AMBIENT, lowAmbient);
glLightfv(GL_LIGHT0, GL_DIFFUSE, lowDiffuse);

//在場景中繪製物體
DrawModels()

PS:此時咱們並不須要交換緩衝區(swapbuffers).

若是顯示出來是這樣子的。

image

有些OpenGL實現支持一種GL_ARB_shadow_ambient擴展,它可使咱們沒必要進行第一遍的陰影繪圖。

 

而後是光照

目前咱們有了一個很昏暗的場景,要製造陰影,我須要一個明亮的光照區域,來與陰影區造成對比。如何決定這個接受更強光照的區域是陰影貼圖的關鍵。在這個明亮的區域,咱們用兩倍於陰影的光照強度進行繪製。

GLfloat ambientLight[] = { 0.2f, 0.2f, 0.2f, 1.0f};
GLfloat diffuseLight[] = { 0.7f, 0.7f, 0.7f, 1.0f};
...
glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);

 

這樣獲得的陰影不全是黑色的。


若是去掉前面的繪製陰影的結果是:


投影陰影貼圖

咱們的目的是須要把陰影貼圖投影到場景中(從照相機的位置看)。投影這些表明着光源到被光照射到的第一個物體的距離的深度值。把紋理座標重定向到正確的座標空間須要一些數學知識。以前咱們解釋了把頂點從物體空間變換到視覺空間,再變換到裁剪空間,而後變換到規格化的設備座標,最後變換到窗口空間的過程。在這裏有兩組不一樣的變換矩陣,一組用於變換到照相機的視覺空間,一組用於變換到光源的視覺空間。經過這兩組矩陣變換獲得兩個從不一樣角度觀察的場景。

image

上面的箭頭表示了咱們須要應用到視覺線性紋理座標的變換過程。紋理的投影一般是從視覺線性座標的產生開始的。這個過程是自動產生紋理座標的。不一樣於物體線性紋理座標的生成,視覺線性座標的生成並不固定到任何幾何圖形之上。反之,它好像是一臺投影儀把紋理投影到場景中,想象一下你在投影儀前走動的時候,屏幕上會出現不規則的身體形狀。

投影紋理映射:

image

如今咱們得到在照相機的視覺空間下頂點對應的紋理座標。那咱們須要進行一些變換來獲得頂點的紋理座標。當前咱們在照相機機的視覺空間,首先咱們經過視圖矩陣的逆變換回到世界座標系,而後再變換到光源的視覺空間,而後到光源的裁剪空間。這一系列的變換能夠經過下面的矩陣相乘獲得:

M = Plight * MVlight * MVcamara-1

裁剪空間規格化後的x,y,z的座標範圍在[-1, 1]之間,然而咱們的紋理座標範圍爲[0,1],因此咱們還須要把[-1,1]變換到[0,1]的範圍,這個變換很簡單,咱們只須要把[-1,1]縮放一半(S),而後偏移0.5就能夠獲得[0,1]了(B)。

M = B * S * Plight * MVlight * MVcamara-1

因此咱們能夠獲得頂點通過變換後的紋理座標。T1 = M * T;

圖解過程以下:

imageimage

 

imageimage

image

PS: 當前模型視圖矩陣的逆矩陣的乘法操做已經包含在了視覺平面方程式中。

即在OpenGL的紋理自動生成模式GL_EYE_LINEAR中,每個覺平面方程式(eye plane equation)會自動乘以MVcamara-1 

實現上面的步驟一種方式是手動的經過glTranslatef, glScalef, glMultMatrixf 來一步步的實現。另外一個方式是在紋理自動生成中,咱們能夠經過設置一個紋理矩陣來實現上面的變換,把這個紋理矩陣做爲視覺線性座標的視覺平面方程GL_EYE_PLANE便可。

image

M = B * S * Plight * MVlight 大體代碼以下:

M3DMatrix44f tempMatrix;
m3dLoadIdentity44(tempMatrix);
//偏移0.5
m3dTranslateMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
//縮放0.5
m3dScaleMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
//乘以光源投影矩陣
m3dMatrixMultiply44(textureMatrix, tempMatrix, lightProjection);
//乘以光源視圖矩陣
m3dMatrixMultiply44(tempMatrix, textureMatrix, lightModelView);
//矩陣轉置,得到平面方程的s,t,r和q行
m3dTransposeMatrix44(textureMatrix, tempMatrix);

應用到視覺平面中:

//由於在當前模型視圖矩陣的逆矩陣的乘法操做已經包含在了視覺平面方程式中
//確保在glTexGenfv前已經設置好照相機的模型視圖矩陣。
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2],
0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
...

//爲陰影貼圖的投影設置視覺平面
glEnable(GL_TEXTURE_GEN_S);
glEnable(GL_TEXTURE_GEN_T);
glEnable(GL_TEXTURE_GEN_R);
glEnable(GL_TEXTURE_GEN_Q);
glTexGenfv(GL_S, GL_EYE_PLANE, &textureMatrix[0]);
glTexGenfv(GL_T, GL_EYE_PLANE, &textureMatrix[4]);
glTexGenfv(GL_R, GL_EYE_PLANE, &textureMatrix[8]);
glTexGenfv(GL_Q, GL_EYE_PLANE, &textureMatrix[12]);
...
glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);
glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_EYE_LINEAR);

陰影比較

如今咱們如何知道從照相機視角看到的點是否在陰影中呢。從上面的那些步驟來看,咱們已知頂點的深度紋理座標,那麼這個深度紋理座標對應的在深度紋理的值咱們能夠知道即texture[s/q, t/q],這個深度紋理記錄了在光的角度看過去離光源最近的點的深度值,咱們是設置的深度比較函數是glDepthFunc(GL_LEQUAL);。,同時咱們知道(r/q)是頂點在真實光源中深度值,已經經過縮放和偏移變換到了[0,1]的範圍。而後咱們比較texture[s/q, t/q]和(r/q)若是texture[s/q, t/q] < r/q那麼就表示這個點在陰影中。以下圖:

image

深度紋理只包含了一個值表明深度。但在紋理環境的紋理查詢中,咱們須要返回四個成分的值(RGBA)。OpenGL提供了幾種方式把這單個深度值擴展到其餘的通道中,其中包含GL_ALPHA(0,0,0,D),GL_LUMINANCE(D,D,D,1)和GL_INTENSITY(D,D,D,D)。在這裏咱們把深度值擴展到全部的深度通道。

glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INSTENSITY);

在OpenGL中開啓陰影比較,來產生陰影效果。咱們把深度值與紋理座標的R成分進行比較。

//設置陰影比較

glEnable(GL_TEXTURE_2D);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);

效果:

image

書中部分的代碼示例:

// Called to regenerate the shadow map
void RegenerateShadowMap(void)
{
  GLfloat lightToSceneDistance, nearPlane, fieldOfView;
  GLfloat lightModelview[16], lightProjection[16];
  GLfloat sceneBoundingRadius = 95.0f; // based on objects in scene

  // Save the depth precision for where it's useful
  lightToSceneDistance = sqrt(lightPos[0] * lightPos[0] + 
    lightPos[1] * lightPos[1] + 
    lightPos[2] * lightPos[2]);
  nearPlane = lightToSceneDistance - sceneBoundingRadius;
  // Keep the scene filling the depth texture
  fieldOfView = (GLfloat)m3dRadToDeg(2.0f * atan(sceneBoundingRadius / lightToSceneDistance));

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  gluPerspective(fieldOfView, 1.0f, nearPlane, nearPlane + (2.0f * sceneBoundingRadius));
  glGetFloatv(GL_PROJECTION_MATRIX, lightProjection);
  // Switch to light's point of view
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(lightPos[0], lightPos[1], lightPos[2], 
    0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
  glGetFloatv(GL_MODELVIEW_MATRIX, lightModelview);
  glViewport(0, 0, shadowWidth, shadowHeight);

  // Clear the depth buffer only
  glClear(GL_DEPTH_BUFFER_BIT);

  // All we care about here is resulting depth values
  glShadeModel(GL_FLAT);
  glDisable(GL_LIGHTING);
  glDisable(GL_COLOR_MATERIAL);
  glDisable(GL_NORMALIZE);
  glColorMask(0, 0, 0, 0);

  // Overcome imprecision
  glEnable(GL_POLYGON_OFFSET_FILL);

  // Draw objects in the scene except base plane
  // which never shadows anything
  DrawModels(GL_FALSE);

  // Copy depth values into depth texture
  glCopyTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, 
    0, 0, shadowWidth, shadowHeight, 0);

  // Restore normal drawing state
  glShadeModel(GL_SMOOTH);
  glEnable(GL_LIGHTING);
  glEnable(GL_COLOR_MATERIAL);
  glEnable(GL_NORMALIZE);
  glColorMask(1, 1, 1, 1);
  glDisable(GL_POLYGON_OFFSET_FILL);

  // Set up texture matrix for shadow map projection,
  // which will be rolled into the eye linear
  // texture coordinate generation plane equations
  M3DMatrix44f tempMatrix;
  m3dLoadIdentity44(tempMatrix);
  m3dTranslateMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
  m3dScaleMatrix44(tempMatrix, 0.5f, 0.5f, 0.5f);
  m3dMatrixMultiply44(textureMatrix, tempMatrix, lightProjection);
  m3dMatrixMultiply44(tempMatrix, textureMatrix, lightModelview);
  // transpose to get the s, t, r, and q rows for plane equations
  m3dTransposeMatrix44(textureMatrix, tempMatrix);
}

// Called to draw scene
void RenderScene(void)
{
  // Track camera angle
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  if (windowWidth > windowHeight)
  {
    GLdouble ar = (GLdouble)windowWidth / (GLdouble)windowHeight;
    glFrustum(-ar * cameraZoom, ar * cameraZoom, -cameraZoom, cameraZoom, 1.0, 1000.0);
  }
  else
  {
    GLdouble ar = (GLdouble)windowHeight / (GLdouble)windowWidth;
    glFrustum(-cameraZoom, cameraZoom, -ar * cameraZoom, ar * cameraZoom, 1.0, 1000.0);
  }

  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
  gluLookAt(cameraPos[0], cameraPos[1], cameraPos[2], 
    0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);

  glViewport(0, 0, windowWidth, windowHeight);

  // Track light position
  glLightfv(GL_LIGHT0, GL_POSITION, lightPos);

  // Clear the window with current clearing color
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  if (showShadowMap)
  {
    // Display shadow map for educational purposes
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();
    glMatrixMode(GL_TEXTURE);
    glPushMatrix();
    glLoadIdentity();
    glEnable(GL_TEXTURE_2D);
    glDisable(GL_LIGHTING);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_NONE);
    // Show the shadowMap at its actual size relative to window
    glBegin(GL_QUADS);
    glTexCoord2f(0.0f, 0.0f);
    glVertex2f(-1.0f, -1.0f);
    glTexCoord2f(1.0f, 0.0f);
    glVertex2f(((GLfloat)shadowWidth/(GLfloat)windowWidth)*2.0f-1.0f, 
      -1.0f);
    glTexCoord2f(1.0f, 1.0f);
    glVertex2f(((GLfloat)shadowWidth/(GLfloat)windowWidth)*2.0f-1.0f, 
      ((GLfloat)shadowHeight/(GLfloat)windowHeight)*2.0f-1.0f);
    glTexCoord2f(0.0f, 1.0f);
    glVertex2f(-1.0f, 
      ((GLfloat)shadowHeight/(GLfloat)windowHeight)*2.0f-1.0f);
    glEnd();
    glDisable(GL_TEXTURE_2D);
    glEnable(GL_LIGHTING);
    glPopMatrix();
    glMatrixMode(GL_PROJECTION);
    gluPerspective(45.0f, 1.0f, 1.0f, 1000.0f);
    glMatrixMode(GL_MODELVIEW);
  }
  else if (noShadows)
  {
    // Set up some simple lighting
    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);

    // Draw objects in the scene including base plane
    DrawModels(GL_TRUE);
  }
  else
  {
    if (!ambientShadowAvailable)
    {
      GLfloat lowAmbient[4] = {0.1f, 0.1f, 0.1f, 1.0f};
      GLfloat lowDiffuse[4] = {0.35f, 0.35f, 0.35f, 1.0f};

      // Because there is no support for an "ambient"
      // shadow compare fail value, we'll have to
      // draw an ambient pass first...
      glLightfv(GL_LIGHT0, GL_AMBIENT, lowAmbient);
      glLightfv(GL_LIGHT0, GL_DIFFUSE, lowDiffuse);

      // Draw objects in the scene, including base plane
      DrawModels(GL_TRUE);

      // Enable alpha test so that shadowed fragments are discarded
      glAlphaFunc(GL_GREATER, 0.9f);
      glEnable(GL_ALPHA_TEST);
    }

    glLightfv(GL_LIGHT0, GL_AMBIENT, ambientLight);
    glLightfv(GL_LIGHT0, GL_DIFFUSE, diffuseLight);

    // Set up shadow comparison
    glEnable(GL_TEXTURE_2D);
    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, 
      GL_COMPARE_R_TO_TEXTURE);

    // Set up the eye plane for projecting the shadow map on the scene
    glEnable(GL_TEXTURE_GEN_S);
    glEnable(GL_TEXTURE_GEN_T);
    glEnable(GL_TEXTURE_GEN_R);
    glEnable(GL_TEXTURE_GEN_Q);
    glTexGenfv(GL_S, GL_EYE_PLANE, &textureMatrix[0]);
    glTexGenfv(GL_T, GL_EYE_PLANE, &textureMatrix[4]);
    glTexGenfv(GL_R, GL_EYE_PLANE, &textureMatrix[8]);
    glTexGenfv(GL_Q, GL_EYE_PLANE, &textureMatrix[12]);

    // Draw objects in the scene, including base plane
    DrawModels(GL_TRUE);

    glDisable(GL_ALPHA_TEST);
    glDisable(GL_TEXTURE_2D);
    glDisable(GL_TEXTURE_GEN_S);
    glDisable(GL_TEXTURE_GEN_T);
    glDisable(GL_TEXTURE_GEN_R);
    glDisable(GL_TEXTURE_GEN_Q);
  }

  if (glGetError() != GL_NO_ERROR)
    fprintf(stderr, "GL Error!\n");

  // Flush drawing commands
  glutSwapBuffers();
}

完整代碼地址https://github.com/sweetdark/openglex/tree/master/shadowmap

表述能力有限。若是錯誤,請指正不勝感激。詳細的請參考下面的連接。

投影映射紋理GL_EYE_LINEAR的參考:

http://blog.csdn.net/liu_lin_xm/article/details/4850526

http://blog.csdn.net/xukunn1226/article/details/775644

英文http://www.nvidia.com/object/Projective_Texture_Mapping.html

陰影貼圖的參考:

http://www.eng.utah.edu/~cs5610/lectures/ShadowMapping%20OpenGL%202009.pdf

ftp://download.nvidia.com/developer/presentations/2004/GPU_Jackpot/Shadow_Mapping.pdf

相關文章
相關標籤/搜索