之前我們介紹過簡單的把物體壓平到投影平面來制造陰影。但這種陰影方式有其局限性(如投影平面須是平面)。在OpenGL1.4引入了一種新的方法陰影貼圖來產生陰影。
陰影貼圖背後的原理是簡單的。我們先把光源的位置當作照相機的位置,我們從這個位置觀察物體,我們就知道哪些物體的表面是被照射到(被光源看到)的,哪些是沒有被照射到(被遮擋住)的(在某個方向上離光源最近的表面是被照射的,後面的表面則沒有被照射到)。我們開啟深度測試,這樣我們就可以得到一個有用的深度緩沖區數據(每一個像素在深度緩沖區中的結果),然後我們從深度緩沖區中讀取數據作為一個陰影紋理,投影回場景中,然後我們在使用照相機的視角,來渲染物體。
首先我們把視角移到光源的位置。我們可以通過glu庫的輔助函數:
gluLookAt(lightPos[0], lightPos[1], lightPos[2], 0.0f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f);
把光源的位置設置為觀察的位置。
為了以最佳的方式利用空間來產生陰影貼圖。從光源的角度看過去的透視可視區域要適應窗口的比例,且透視的最近平面位置是裡光源最近的物體的平面,最遠的平面位置是離光源最遠的物體的平面。這樣我們就可以充分的利用場景的信息來填充深度緩沖區,來制造陰影貼圖。我們估計恰好包好整個場景的視野。
//場景的半徑大小 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的圓中。這是我們對場景的粗略估計。大致如下圖:
因為我們只需要得到像素經過深度測試後,深度緩沖區的結果。所以我們可以去掉一切不必要的的細節,不往顏色緩沖區中寫數據因為不需要顯示。
glShadeModel(GL_FLAT); glDisable(GL_LIGHTING); glDisable(GL_COLOR_MATERIAL); glDisable(GL_NORMALIZE); glColorMask(0,0,0,0); ...
如果我們可以看到深度緩沖區,深度緩沖區的灰度圖大概是這樣子的。
我們需要拷貝深度的數據到紋理中作為陰影貼圖。在OpenGL1.4之後,glCopyTexImage2D允許我們從深度緩沖區中拷貝數據。紋理數據多了一種深度紋理的類型,其內部格式包括GL_DEPTH_COMPONENT16,GL_DEPTH_COMPONENT24,GL_DEPTH_COMPONENT32,數字代表每個紋理單元包含的位數。一般情況下,我們希望其內部格式與深度緩沖區的精度相匹配。OpenGL允許你指定通用的GL_DEPTH_COMPONENT格式來匹配你的深度緩沖區。在以光源的視角繪制後,我們把深度緩沖區的數據拷貝出來作為深度紋理:
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).
如果顯示出來是這樣子的。
有些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);
這樣得到的陰影不全是黑色的。
如果去掉前面的繪制陰影的結果是:
我們的目的是需要把陰影貼圖投影到場景中(從照相機的位置看)。投影這些代表著光源到被光照射到的第一個物體的距離的深度值。把紋理坐標重定向到正確的坐標空間需要一些數學知識。之前我們解釋了把頂點從物體空間變換到視覺空間,再變換到裁剪空間,然後變換到規格化的設備坐標,最後變換到窗口空間的過程。在這裡有兩組不同的變換矩陣,一組用於變換到照相機的視覺空間,一組用於變換到光源的視覺空間。通過這兩組矩陣變換得到兩個從不同角度觀察的場景。
上面的箭頭表示了我們需要應用到視覺線性紋理坐標的變換過程。紋理的投影通常是從視覺線性坐標的產生開始的。這個過程是自動產生紋理坐標的。不同於物體線性紋理坐標的生成,視覺線性坐標的生成並不固定到任何幾何圖形之上。反之,它好像是一台投影儀把紋理投影到場景中,想象一下你在投影儀前走動的時候,屏幕上會出現不規則的身體形狀。
投影紋理映射:
現在我們獲得在照相機的視覺空間下頂點對應的紋理坐標。那我們需要進行一些變換來得到頂點的紋理坐標。當前我們在照相機機的視覺空間,首先我們通過視圖矩陣的逆變換回到世界坐標系,然後再變換到光源的視覺空間,然後到光源的裁剪空間。這一系列的變換可以通過下面的矩陣相乘得到:
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;
圖解過程如下:
PS: 當前模型視圖矩陣的逆矩陣的乘法操作已經包含在了視覺平面方程式中。
即在OpenGL的紋理自動生成模式GL_EYE_LINEAR中,每一個覺平面方程式(eye plane equation)會自動乘以MVcamara-1
實現上面的步驟一種方式是手動的通過glTranslatef, glScalef, glMultMatrixf 來一步步的實現。另一個方式是在紋理自動生成中,我們可以通過設置一個紋理矩陣來實現上面的變換,把這個紋理矩陣作為視覺線性坐標的視覺平面方程GL_EYE_PLANE即可。
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那麼就表示這個點在陰影中。如下圖:
深度紋理只包含了一個值代表深度。但在紋理環境的紋理查詢中,我們需要返回四個成分的值(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);
效果:
書中部分的代碼示例:
// 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://www.linuxidc.com/Linux/2015-02/113974.htm
英文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
OpenGL超級寶典 第4版 中文版PDF+英文版+源代碼 見 http://www.linuxidc.com/Linux/2013-10/91413.htm
OpenGL編程指南(原書第7版)中文掃描版PDF 下載 http://www.linuxidc.com/Linux/2012-08/67925.htm
OpenGL 渲染篇 http://www.linuxidc.com/Linux/2011-10/45756.htm
Ubuntu 13.04 安裝 OpenGL http://www.linuxidc.com/Linux/2013-05/84815.htm
OpenGL三維球體數據生成與繪制【附源碼】 http://www.linuxidc.com/Linux/2013-04/83235.htm
Ubuntu下OpenGL編程基礎解析 http://www.linuxidc.com/Linux/2013-03/81675.htm
如何在Ubuntu使用eclipse for c++配置OpenGL http://www.linuxidc.com/Linux/2012-11/74191.htm
更多《OpenGL超級寶典學習筆記》相關知識 見 http://www.linuxidc.com/search.aspx?where=nkey&keyword=34581