OpenGL 陰影之Shadow Mapping和Shadow Volumes


  先說下開發環境.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);        
    }
plane VBO輸出

  下面是可編程管線的初始化代碼.

            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();
glsl 正常輸出
#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;
}
glsl 頂點着色器

  我們可以看到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;
}
glsl 像素着色器

  在像素着色器中,紋理坐標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);
Shadow Volumes正常渲染模型

  第二次我們得到陰影區,關閉顏色緩沖區可寫,關閉深度緩沖區可寫(注意深度測試還是打開的,意思可以比較現在寫入的深度與以前的深度,但是不會更新到深度緩沖區),打開模版測試,重置模板緩沖區內所有數據為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);
zfail得到陰影區

  參考:

  Shadow Map陰影貼圖技術之探系列

  Shadow Volume 陰影錐技術之探系列

  OpenGL陰影,Shadow Mapping(附源程序)

  OpenGL陰影,Shadow Volumes(附源程序,使用 VCGlib )

  [轉]陰影錐原理與展望—真實的游戲效果的實現

  Shadow Volume(陰影錐)技術詳解

  下面是附件,其中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 打不開或是出錯請聯系我.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM