先說下開發環境.VS2013,C++空項目,引用glut,glew.glut包含基本窗口操作,免去我們自己新建win32窗口一些操作.glew使我們能使用最新opengl的API,因winodw本身只包含opengl 1.1版本的API,根本是不能用的.
其中矩陣計算采用gitHub項目openvr中的三份文件, Vectors.h ,Matrices.h, Matrices.cpp,分別是矢量與點類,矩陣類,我們需要的一些操作,矢量的叉乘和點乘,矩陣轉置,矩陣的逆,矩陣與矢量相剩等.
這里主要簡單介紹這二種陰影實現.Shadow Mapping簡單來說,就是以燈光為視角,得到整個場景的深度圖(深度圖請看下面一段仔細說明).然后在正常視角下,把頂點轉化成原燈光視角下的頂點,根據頂點位置找到深度紋理中存放的深度,如果頂點的深度值大於紋理中的深度值(說明在燈光視角中,頂點上有遮擋物,如下圖),說明在陰影范圍內.
(此圖引用博友http://www.cnblogs.com/liangliangh/p/4131103.html中圖片)
在這里,有必要講一下深度圖,不然有些位置大家可能理解不了,這個深度圖是全屏渲染圖,意思就是是場景中的三維物體經過投影成二維,這樣就達到一種效果,紋理坐標與三維物體的坐標是有對應關系的,簡單來說,三維物體經過投影后,我們經過(xyzw)/w,這樣x,y,z 都在(-1,1)之間,再經過乘0.5加0.5后對應(0,1)之間,也就是深度圖的紋理坐標,這過程和3D中物體由局部坐標到屏幕坐標的變換(屏幕Y是從上到下,還需要轉換,這里先不說)一樣.那么深度圖一共包含了二樣關系,一是紋理坐標st,對應3維中頂點xy.二是深度圖本身保存的深度,這個深度是經過深度測試和深度寫入(所以這二個GL_DEPTH_TEST, glDepthMask記的打開)的深度,默認的是深度比較算法是畫家算法(GL_LESS,不要改),意思是深度度上全是最近的深度.
這樣深度圖就是一個三維場景,結合攝像機的設置,就可以把這個場景所有像素都重新投影到三維空間中去.
在附件中, Shadow Mapping主要有二種實現,一種是固定管線,一種是可編程管線,我們先看下固定管線的實現流程,再對比可編程管線的實現來理解整個過程.
如下是固定管線中紋理初始化的設置.

glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); glGenTextures(1, &shadowTexture); glBindTexture(GL_TEXTURE_2D, shadowTexture); // 紋理和光照計算結果相乘 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); 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_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //GL_LUMINANCE 把深度值替換到RGB三個分量上,GL_INTENSITY則替換到RGBA四個分量上.(深度值只有一個) //簡單來說,就是定義深度如何保存,如果是GL_ALPHA,則替換到第四個分量上. //glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);// GL_LUMINANCE); // This is to allow usage of shadow2DProj function in the shader //紋理本身存的是深度值,而紋理坐標經過轉換后成對應點的坐標. 紋理坐標R點比較紋理本身 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);//GL_NONE GL_COMPARE_R_TO_TEXTURE //比較方法,少於或等於是1,大於是0 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);//GL_GEQUAL,GL_LEQUAL //使用API自動生成的紋理坐標 glEnable(GL_TEXTURE_GEN_S); glEnable(GL_TEXTURE_GEN_T); glEnable(GL_TEXTURE_GEN_R); glEnable(GL_TEXTURE_GEN_Q); glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); glTexGeni(GL_R, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); glTexGeni(GL_Q, GL_TEXTURE_GEN_MODE, GL_OBJECT_LINEAR); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowWidth, shadowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0); //FBO 把楨緩沖區的深度輸出到shadowTexture紋理中 glGenFramebuffers(1, &frameBuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBuffer); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowTexture, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); glBindTexture(GL_TEXTURE_2D, 0);
其中有幾個主要設置拿出來說下:
glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_LUMINANCE);
這個是讓紋理值替換深度到那些分量上,我們知道紋理一個像素是4分量,分別是rgba,其中GL_LUMINANCE把深度復制到rgb中, GL_INTENSITY 則是rgba中, GL_ALPHA復制到a中,這個我試了,在固定管線下,不設置也行,在可管程管線中,則要根據設置的值取不同的分量.
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
這二設定一個是指明紋理是是引用貼圖對比模式,指明紋理坐標R(strq,r是紋理第三分量)對比紋理本身的值.第二個設置是第一對比模式的補充,說明小於或等於是1,而大於是0.
最后glTexGeni指明紋理坐標生成方式, GL_OBJECT_LINEAR指明是在模型空間內,頂點坐標拿來紋理坐標.
在這里,我們把楨緩沖區的數據轉出到紋理,采用的是FBO的方式,如果硬件不能使用FBO,大家可以改寫用Pbuffer或CopyPixels的方式.
然后在渲染時,我們首先以燈光做為視點,生成視圖坐標,選擇一個合適的透視矩陣,輸出深度到深度紋理中.

//寫入深度到FBO中,以燈光為視角 glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBuffer); //顏色不需要輸出 glColorMask(false, false, false, false); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glViewport(0, 0, shadowWidth, shadowHeight); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(90.0f, shadowWidth / shadowHeight, 1.0f, 1000.0f); glGetFloatv(GL_PROJECTION_MATRIX, lightProjection); glMatrixMode(GL_MODELVIEW); glLoadIdentity(); gluLookAt(lightpos.x, lightpos.y, lightpos.z, 0.f, 0.f, 0.f, 0.f, 1.0f, 0.f); glGetFloatv(GL_MODELVIEW_MATRIX, lightModelview); this->drawModel(true); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
然后我們需要記錄當前MVP矩陣,在前面說過,MVP后要轉化(-1,1)到(0,1),所以前面還需要*0.5+0.5.紋理坐標生成采用GL_OBJECT_LINEAR,下面為了glTexGenfv傳參方便,我們轉置一下矩陣.這樣頂點經過這個轉換后,自動生成的紋理坐標其實是對應的原燈光視圖下的頂點.

//紋理矩陣變換 自動生成紋理坐標轉化成頂點坐標 float tempMat[16]; glPushMatrix(); glLoadIdentity(); glTranslatef(0.5f, 0.5f, 0.5f); glScalef(0.5f, 0.5f, 0.5f); // Proj * Model 將紋理坐標轉到世界空間 glMultMatrixf(lightProjection); glMultMatrixf(lightModelview); glGetFloatv(GL_MODELVIEW_MATRIX, tempMat); glLoadTransposeMatrixf(tempMat); glGetFloatv(GL_MODELVIEW_MATRIX, tempMat); glPopMatrix();
最后我們正常輸出場景,在這里,記得前面設定的GL_COMPARE_R_TO_TEXTURE不,紋理坐標大於紋理深度值則是陰影.

//正常輸出 glColorMask(true, true, true, true); glViewport(0, 0, widht, height); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(45.0f, (GLfloat)this->widht / (GLfloat)this->height, 0.1f, 100.0f); //加載視圖矩陣 this->mcamera->lookat(); //添加一個點光源. float pos[4] = { lightpos.x, lightpos.y, lightpos.z, 1.0 }; glLightfv(GL_LIGHT0, GL_POSITION, pos); //輸出地面,采用模型本身的紋理坐標 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, planeTexture); //輸出地面,采用API自動生成的紋理坐標 glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, shadowTexture); glTexGenfv(GL_S, GL_OBJECT_PLANE, &tempMat[0]); glTexGenfv(GL_T, GL_OBJECT_PLANE, &tempMat[4]); glTexGenfv(GL_R, GL_OBJECT_PLANE, &tempMat[8]); glTexGenfv(GL_Q, GL_OBJECT_PLANE, &tempMat[12]); mplane->draw(); //記的關掉紋理,不然會影響下面模型 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, 0); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, 0); //輸出球,燈,茶壺 sphLight->position.x = lightpos.x; sphLight->position.y = lightpos.y; sphLight->position.z = lightpos.z; this->drawModel(false); glutSwapBuffers();
說起來就是這么回事,但是仔細回想下,其實完全都是由openGL內部實現,我們完全搞不清楚真的是怎么實現的,我們用的也是一些API,大家可能也對實現過程N多疑惑,那么下面可編程管線實現的Shadow Mapping能讓我們完全搞清楚怎么回事,也沒有這些GL_COMPARE_R_TO_TEXTURE, GL_LEQUAL,紋理坐標自動生成這些完全不知道內部操作的設定.
首先我們需要對Plane改寫,支持VBO渲染,增加一個類glslprogram用於管理着色器相關.

void plane::drawShader(int pos, int tex) { if (!this->bCreate) { this->init(); } mat->draw(); glBindBuffer(GL_ARRAY_BUFFER, vboId); glEnableVertexAttribArray(pos); glEnableVertexAttribArray(tex); glVertexAttribPointer(tex, 2, GL_FLOAT, false, 20, (void*)0); glVertexAttribPointer(pos, 3, GL_FLOAT, false, 20, (void*)8); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboId); glDrawElements(GL_TRIANGLES, indexCount, GL_UNSIGNED_INT, 0); glDisableVertexAttribArray(pos); glDisableVertexAttribArray(tex); }
下面是可編程管線的初始化代碼.

glActiveTexture(GL_TEXTURE1); glGenTextures(1, &shadowTexture); glBindTexture(GL_TEXTURE_2D, shadowTexture); // 紋理和光照計算結果相乘 glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_BLEND); 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_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); //GL_LUMINANCE 把深度值替換到RGB三個分量上,GL_INTENSITY則替換到RGBA四個分量上.(深度值只有一個) //簡單來說,就是定義深度如何保存,如果是GL_ALPHA,則替換到第四個分量上. glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_ALPHA);// GL_LUMINANCE); glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, shadowWidth, shadowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, 0); glBindTexture(GL_TEXTURE_2D, 0); //FBO 把楨緩沖區的深度輸出到shadowTexture紋理中 glGenFramebuffers(1, &frameBuffer); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, frameBuffer); glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, shadowTexture, 0); glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
可以發現,少了很多前面單獨拿出來說的設定,這里只是讓FBO把深度輸出到紋理中,紋理中用float保存.
輸出FBO同上面一樣,不同的正常輸出場景,在這里,我們設定好着色器相關的參數.傳入頂點着色器中.

//輸出地面,采用模型本身的紋理坐標 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, planeTexture); //輸出地面,采用API自動生成的紋理坐標 glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, shadowTexture); auto svM = Matrix4::setCamera(lightpos.x, lightpos.y, lightpos.z, 0.f, 0.f, 0.f); auto spM = Matrix4::setFrustum(90.0f, shadowWidth / shadowHeight, 1.0f, 1000.0f); auto eye = mcamera->getEye(); auto target = mcamera->getTarget(); auto vM = Matrix4::setCamera(eye.x, eye.y, eye.z, target.x, target.y, target.z); auto pM = Matrix4::setFrustum(45.0f, (GLfloat)this->widht / (GLfloat)this->height, 0.1f, 100.0f); Matrix4 mM; mM.translate(mplane->position.x, mplane->position.y, mplane->position.z); program.enable(); program.setUniformMatrix("shadowViewMat", svM.get()); program.setUniformMatrix("shadowProMat", spM.get()); program.setUniformMatrix("nm", mM.get()); program.setUniformMatrix("nv", vM.get()); program.setUniformMatrix("np", pM.get()); program.setUniform("normal", 0.f, 1.f, 0.f); program.setUniform("texture2D", 0); program.setUniform("uShadowMap", 1); mplane->drawShader(0, 1); program.disable();

#version 330 compatibility uniform mat4 shadowViewMat; uniform mat4 shadowProMat; uniform mat4 nm; uniform mat4 nv; uniform mat4 np; layout(location = 1)in vec2 iTexCoord; layout(location = 0)in vec3 ipos; out vec4 oShadowTexCoord; out vec2 oTexCoord; out vec4 wordPos; void main() { vec4 mvpPos = np*nv*nm * vec4(ipos,1.0); //陰影繪制中的透視坐標x,y,z in (-1,1) vec4 shadowTex = shadowProMat*shadowViewMat* nm * vec4(ipos,1.0); //(x,y,z,w) -> (x/w,y/w,z/w,1) = (x,y,z in [-1,1]) oShadowTexCoord = shadowTex / shadowTex.w; //(-1,1) To (0,1)紋理坐標 這樣紋理坐標rt就對應點x,y oShadowTexCoord = 0.5 * oShadowTexCoord + 0.5; wordPos = nm * vec4(ipos,1.0); gl_Position = mvpPos; oTexCoord = iTexCoord; }
我們可以看到oShadowTexCoord代表頂點(這是正常場景坐標)轉化成原燈光視圖下的坐標.並轉化到(0,1)之間,然后到像素着色器中.

#version 330 compatibility //uniform sampler2DShadow uShadowMap; uniform sampler2D uShadowMap; uniform sampler2D texture2D; uniform vec3 normal; in vec4 oShadowTexCoord; in vec2 oTexCoord; in vec4 wordPos; void main() { float noshadow = 1.0; //深度紋理中的深度值. float depth = texture(uShadowMap, oShadowTexCoord.xy).a; //現在在渲染的頂點深度值 float depth1 = oShadowTexCoord.z; if(depth < depth1) noshadow = 0.5; //紋理顏色 vec4 textColor = texture(texture2D,oTexCoord); //外部環境光 vec4 color = gl_FrontMaterial.ambient * gl_LightModel.ambient; //燈光散射光 vec4 diffuse = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; vec4 ambient = gl_FrontMaterial.ambient * gl_LightSource[0].ambient; vec3 halfV = normalize(gl_LightSource[0].halfVector.xyz); vec3 lightdir = normalize( vec3(gl_LightSource[0].position) - wordPos.xyz); float dist = length(vec3(gl_LightSource[0].position) - wordPos.xyz); float NdotL = max(0.0, dot(lightdir, normal)); if (NdotL > 0.0) { float att = 1.0 / (gl_LightSource[0].constantAttenuation + gl_LightSource[0].linearAttenuation * dist + gl_LightSource[0].quadraticAttenuation * dist * dist); color += att * (diffuse * NdotL + ambient); float specular = max(dot(normal,halfV),0.0); specular = pow(specular, gl_FrontMaterial.shininess); color += att * gl_FrontMaterial.specular * gl_LightSource[0].specular * specular; } vec3 rgb = color.rgb * textColor.rgb*2; gl_FragColor = vec4(rgb,textColor.a)*noshadow; }
在像素着色器中,紋理坐標st取出深度值a(前面我設定GL_DEPTH_TEXTURE_MODE為GL_ALPHA,想一下,取r,g,b會不會有效果),然后比較紋理坐標r與紋理值來判斷是否有陰影.其中有一段光照代碼,在這就沒必要看了,只要知道生成光照就行了,在像素着色器中,陰影顯示成什么顏色我們能也完全控制了,通過着色器代碼的實現,我們應該很清楚Shadow Mapping是如何工作的了.
當然在可編程管線下,我們一樣是可以使用GL_TEXTURE_COMPARE_MODE- GL_COMPARE_R_TO_TEXTURE的,這樣我們需要把sampler2D改寫成sampler2Dshadow,用紋理坐標取出來的深度值就只有0和1了,OpenGL自己幫我們比較了,為0則表示GL_LEQUAL失敗,在陰影中.
說完了Shadow Mapping,我們來了解下Shadow Volumes的原理,如下圖:
簡單來說,就是在燈光與頂點擴展成錐體形式,進入錐體就加模板值1,出去錐體就減模板值1,最后判斷模板值不為0則是陰影區域,原理可以比說Shadow Mapping還簡單,確實Shadow Volumes難的不是理念,更多是如何形成有效簡便的錐體結構.在這說,我們主要講解Shadow Volumes是工作原理,故采用二來簡單的三角形來說明.
在說明代碼之前,我們需要先了解模版緩沖區與模版測試.模版測試屬於片斷處理,在像素着色器之后,先進行Alpha測試后就是模版測試,模版測試后是深度測試,記的模版測試在片斷着色器之后,深度測試之前,這個測試針對的就是模版緩沖區,你可以把模版緩沖區當做和深度緩沖區差不多的東東,每個像素有一個模板值,初始我們一般設為0,有API能對此進行操作.然后我們還需要知道在opengl中,我們把逆時針連接的面稱為正面,另一面就是反面.
Shadow Volumes簡單來說,一般包含三次Pass.三次Pass都需要開啟深度測試.
第一次我們正常渲染模型.先清空顏色,深度,模版緩沖區,然后打開深度緩沖區可寫.先不用打開模版測試.(注意glut也需要在窗口初始化時傳入模板參數)

//第一次PASS 寫入深度 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glClearStencil(0); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); //深度緩沖區可寫 glDepthMask(GL_TRUE); glDisable(GL_STENCIL_TEST); this->mcamera->lookat(); sphLight->position.x = lightpos.x; sphLight->position.y = lightpos.y; sphLight->position.z = lightpos.z; sphLight->draw(); glNormal3f(0, 1, 0); mplane->draw(); glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, material::blue); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, material::green); drawTri(tri1); glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, material::green); glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, material::blue); drawTri(tri2);
第二次我們得到陰影區,關閉顏色緩沖區可寫,關閉深度緩沖區可寫(注意深度測試還是打開的,意思可以比較現在寫入的深度與以前的深度,但是不會更新到深度緩沖區),打開模版測試,重置模板緩沖區內所有數據為0,設定模板測試直接通過,模板緩沖區操作時設定,正面通過深度緩沖區后加1,反面通過測試緩沖區減1. 注意前面說的,深度測試在模板測試之后,模板測試通過了才有深度測試,在這里,我們設定模板直接通過,深度測試不通過不修改,只有模板與深度全通對模板緩沖區修改,所以也叫zpass算法.
下面三圖分別指示正面通過(+1),反面通過(-1)和正反相加的情形:
前二圖是在http://www.yakergong.net/blog/archives/23 中的,不知為啥沒給出我想要的第三圖,我就自己畫了,第一張圖上淡藍色是正面(截體外面)通過深度測試的像素(模板加1),第二張圖是背面通過深度測試的像素(模板減1),第三張圖就是模板值還是1的像素,也就是我們的陰影區域.從上圖知,通過第一張圖(正面測試的)像素個數是第二和第三張圖之和,我們可以驗證.

//第二次pass glPushAttrib(GL_ALL_ATTRIB_BITS); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glDisable(GL_LIGHT0); //glStencilOpSeparate 需要關閉面向裁剪 glDisable(GL_CULL_FACE); //深度可寫關閉 不會覆蓋沒有像素 注意:深度測試一直打開的 glEnable(GL_DEPTH_TEST); glDepthMask(GL_FALSE); glDepthFunc(GL_LESS); //開啟模板測試 glEnable(GL_STENCIL_TEST); //模板比較函數 glStencilFunc(GL_ALWAYS, 0, 0xFF); glClearStencil(0); // p1:面向 p2:模板沒通過測試 p3:模板通過測試,深度測試沒通過 p4:深度測試通過 glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_KEEP, GL_INCR_WRAP); glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_DECR_WRAP); //mplane->draw(); drawTriVolume(tri1); drawTriVolume(tri2); //讀取模版值 //vector<unsigned char> data; //data.resize(widht*height); //glReadPixels(0, 0, widht, height, GL_STENCIL_INDEX, GL_UNSIGNED_BYTE, data.data()); //int index = 0; //for_each(data.begin(), data.end(), [&index](unsigned char x){ // if (x != 0) // { // index++; // } //}); //cout << index << endl;
有些讀取模板取的代碼在上面屏掉,大家可以分別測試glStencilOpSeparate中的1:GL_FRONT啟用,2:GL_BACK啟用,3二者都啟用,看看是否第一種情況的像素值是第二和第三之和.
第三次Pass,渲染上面的陰影區.下面的一些狀態我就不仔細說了,代碼里有注釋,主要是打開模板測試,把整個屏幕刷黑,但是只有模板值為1的像素才能通過測試,更新顏色到楨緩沖區.

//第三次pass 畫陰影,在全屏幕蒙板值不為0的地方畫陰影 glEnable(GL_STENCIL_TEST); glStencilFunc(GL_NOTEQUAL, 0, 0xFF); glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); //2.打開顏色緩存,畫出陰影 glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); //3.開混合,讓陰影顏色和陰影所再物體的本來顏色混合一下 glEnable(GL_BLEND); //glBlendFunc(GL_ONE, GL_ONE); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); //4.全屏畫陰影 在模版測試時,只有前面有陰影的地方才被畫上 glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glOrtho(0, widht, 0, height, -1, 1); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); //全屏黑色 在模版測試時,只有前面有陰影的地方才被畫上 glMaterialfv(GL_FRONT, GL_AMBIENT, material::gray); glRectf(0.0, 0.0, widht, height); glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); glPopAttrib();
zpass主要就是上面的過程,不過有個缺點,就是視點進入到了 shadow volume 里面后,zpass算法就失效了,大家可以移步[轉]陰影錐原理與展望—真實的游戲效果的實現里有詳細說明.在此基礎上,幾個牛人研究出了zfail方法.原理如下圖:
如前面所說,深度測試在模板測試之后,模板測試通過了才有深度測試,如果模板測試通過,深度測試不通過,zfail就是在這步針對模板值修改,因其與zpass大部分相同,只是在第二步pass得到陰影區域的計算不同,故只貼出這部分代碼.

//第二次pass glPushAttrib(GL_ALL_ATTRIB_BITS); glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); glDisable(GL_LIGHT0); //glStencilOpSeparate 需要關閉面向裁剪 glDisable(GL_CULL_FACE); //深度可寫關閉 不會覆蓋沒有像素 注意:深度測試一直打開的 glDepthMask(GL_FALSE); glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); //開啟模板測試 glEnable(GL_STENCIL_TEST); //模板比較函數 glStencilFunc(GL_ALWAYS, 0, 0xFF); glClearStencil(0); // p1:面向 p2:模板沒通過測試 p3:模板通過測試,深度測試沒通過 p4:深度測試通過 glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_INCR_WRAP, GL_KEEP); // 改進后 glStencilOpSeparate(GL_BACK, GL_KEEP, GL_DECR_WRAP, GL_KEEP); //mplane->draw(); drawTriVolume(tri1); drawTriVolume(tri2);
參考:
OpenGL陰影,Shadow Volumes(附源程序,使用 VCGlib )
下面是附件,其中camera提供第一人稱與第三人稱攝像機實現,loadtexture實現了bmp圖片文件的導入,plane與sphere分別對應平面與球的實現,其中window原本是打算用win32實現,后面用glut代替,sample開頭的文件分別對應shadow mapping固定管線,可編程管線與shadow volume中的zpass與zfail實現,這四個類分別是glshow的子類,在main中,直接修改glshow分別是那種子類,就能看各個效果.引用的頭文件與lib,dll全放入lib文件夾下,其中二個dll文件在32位操作環境放入C:\Windows\System32,64位操作系統放入C:\Windows\SysWOW64.
附件:OpenglTest.zip 打不開或是出錯請聯系我.