既然模擬出了光照,那麼也少不了陰影,陰影的產生是因為距離光線較近的物體遮擋了距離較遠的物體,導致被遮擋的物體接受的光照少於遮擋物的,因此陰影的產生與否與物體到光源的位置有關系,靜態物體的陰影可以用光照貼圖來模擬,而動態陰影要用陰影錐或者陰影貼圖實現,陰影錐會引入許多額外的頂點為管線帶來負擔,目前比較流行的陰影模擬方法是用陰影貼圖,它的好處在於只是用紋理存儲物體的深度信息而並不會引入額外頂點.
要實現陰影貼圖有以下幾個步驟:
首先開辟一塊紋理緩存以便之後保存世界的深度信息:
//Create the shadow map texture
glGenTextures(1, &shadowMapTextureLeft);
glBindTexture(GL_TEXTURE_2D, shadowMapTextureLeft);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE_ARB, GL_COMPARE_R_TO_TEXTURE_ARB);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC_ARB, GL_LEQUAL);
glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowMapSize, shadowMapSize,
0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, 0);
glBindTexture(GL_TEXTURE_2D, 0);
我這邊開了兩張深度紋理shadowMapTextureLeft與shadowMapTextureRight.
然後新建渲染目標對象fbo,fbo類似於幀緩沖區,只不過把渲染的片段保存於其他的緩存而不是屏幕上的幀緩沖區:
glGenFramebuffersEXT(1, &fboId);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, fboId);
glDrawBuffer(GL_NONE);
glReadBuffer(GL_NONE);
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT, 0);
這邊的glDrawBuffer(GL_NONE);與glReadBuffer(GL_NONE);是為了屏蔽顏色的輸入輸出,因為這個fbo用於寫入深度信息.
緩沖區准備工作完成.
接著設置光源:
void changeLightPos(float lx,float ly,float lz) {
lightPos.x=lx;
lightPos.y=ly;
lightPos.z=lz;
lightPos.w=0;
}
然後設置渲染陰影貼圖的投影矩陣:
//Calculate light projection matrix
float size=128*2;
lightProjectionMatrixLeft = ortho(-size-size,size-size,-size*2,size*2,-size*2,size*2);
這邊我用的是方向光源,所以設置的平行投影.
然後設置渲染陰影貼圖的視圖矩陣,將視點放在光源位置,將方向定位光向量:
lightViewMatrixLeft=lookAt(lightPos.x, lightPos.y, lightPos.z,
cx, cy, cz,
0.0f, 1.0f, 0.0f);
好了,現在准備工作都完成了,開始渲染陰影貼圖:
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,fboId);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,
GL_DEPTH_ATTACHMENT_EXT,GL_TEXTURE_2D, shadowMapTextureLeft, 0);
//Use viewport the same size as the shadow map
glViewport(0, 0, shadowMapSize, shadowMapSize);
//First pass - from light's point of view
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(lightProjectionMatrixLeft);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(lightViewMatrixLeft);
//Draw back faces into the shadow map
glCullFace(GL_FRONT);
glClear(GL_DEPTH_BUFFER_BIT);
//Draw the scene
callback();
給fbo掛上深度紋理,接下來渲染的內容都將保存到深度紋理當中,把遮擋物的正面剔除,因為遮擋物反面也會產生正確的陰影.
然後還原渲染目標,清除一下幀緩沖區內容
glBindFramebufferEXT(GL_FRAMEBUFFER_EXT,0);
//restore states
glCullFace(GL_BACK);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
接著進行一遍普通渲染,重置投影與視圖矩陣:
glViewport(0, 0, windowWidth, windowHeight);
glMatrixMode(GL_PROJECTION);
glLoadMatrixf(cameraProjectionMatrix);
glMatrixMode(GL_MODELVIEW);
glLoadMatrixf(cameraViewMatrix);
傳入深度紋理進行一次普通渲染:
glActiveTextureARB(GL_TEXTURE1_ARB);
glBindTexture(GL_TEXTURE_2D, shadowMapTextureLeft);
glActiveTextureARB(GL_TEXTURE2_ARB);
glBindTexture(GL_TEXTURE_2D, shadowMapTextureRight);
callback();
glActiveTextureARB(GL_TEXTURE1_ARB);
glBindTexture(GL_TEXTURE_2D, 0);
glActiveTextureARB(GL_TEXTURE2_ARB);
glBindTexture(GL_TEXTURE_2D, 0);
普通渲染的時候使用陰影貼圖比較著色器,進行陰影渲染.
有一點要說明一下,陰影貼圖中的片段深度信息都是在紋理坐標系內的點,那麼進行正常渲染的時候我們傳入渲染陰影貼圖時候用的視圖矩陣與投影矩陣,然後進行以下變換:
模型空間->世界空間->陰影視圖空間->陰影投影空間->規范化設備空間->陰影貼圖紋理空間
那麼接下來向著色器傳遞以下矩陣:
//Calculate texture matrix for projection
//This matrix takes us from eye space to the light's clip space
//It is postmultiplied by the inverse of the current view matrix when specifying texgen
static MATRIX4X4 biasMatrix(0.5f, 0.0f, 0.0f, 0.0f,
0.0f, 0.5f, 0.0f, 0.0f,
0.0f, 0.0f, 0.5f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f); //bias from [-1, 1] to [0, 1]
shadowMatrix=biasMatrix*lightProjectionMatrixLeft*lightViewMatrixLeft;
所以頂點著色器可以這麼寫:
uniform mat4 viewMatrix,modelMatrix,normalMatrix;
uniform mat4 shadowLeftMatrix,shadowRightMatrix;
varying vec4 shadowVertLeft,shadowVertRight;
......
......
shadowVertLeft = shadowLeftMatrix * modelMatrix * gl_Vertex;
shadowVertRight = shadowRightMatrix * modelMatrix * gl_Vertex;
viewVertex = vec3(viewMatrix * modelMatrix * gl_Vertex);
gl_Position = gl_ProjectionMatrix * viewMatrix * modelMatrix * gl_Vertex;
然後在片段著色器當中將傳入的shadowVertLeft和shadowVertRight都除以它們的w坐標值就能得到正常渲染的片元在陰影貼圖坐標系內的坐標值了,之後把這個坐標值的深度(也就是z值)與陰影貼圖內的深度值用shadow2D做比較,這邊我偷了個懶,用shadow2DProj做,使用proj函數可以實現齊次除法,也就是頂點值除以w.
片段著色器這麼寫:
uniform sampler2DShadow texShadowLeft,texShadowRight;
varying vec4 shadowVertLeft,shadowVertRight;
......
......
vec4 texcoordOffset = shadowVertLeft;
if(texcoordOffset.x >= 0.0 && texcoordOffset.y >= 0.0 &&
texcoordOffset.x <= 1.0 && texcoordOffset.y <= 1.0 ) {
texcoordOffset.z+=bias;
float depth = shadow2DProj(texShadowLeft, texcoordOffset).z;
if(depth<1.0 && factor>=0.2)
factor-= 0.1;
if(factor<0.4)
factor=0.4;
}
片段的z坐標要加上一個偏移量,原因如圖所示:
最後返回的factor值即為陰影值,把它與環境顏色相乘就能夠輸出片段了.
最終效果如下:
還能夠在渲染陰影貼圖的那一步使用著色器對深度紋理進行過濾產生半影區,這邊我使用了pcf法對陰影進行處理.
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/Linux/2013-10/91414.htm