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了,为0则表示GL_LEQUAL失败,在阴影中.

  说完了Shadow Mapping,我们来了解下Shadow Volumes的原理,如下图:

  简单来说,就是在灯光与顶点扩展成锥体形式,进入锥体就加模板值1,出去锥体就减模板值1,最后判断模板值不为0则是阴影区域,原理可以比说Shadow Mapping还简单,确实Shadow Volumes难的不是理念,更多是如何形成有效简便的锥体结构.在这说,我们主要讲解Shadow Volumes是工作原理,故采用二来简单的三角形来说明.

  在说明代码之前,我们需要先了解模版缓冲区与模版测试.模版测试属于片断处理,在像素着色器之后,先进行Alpha测试后就是模版测试,模版测试后是深度测试,记的模版测试在片断着色器之后,深度测试之前,这个测试针对的就是模版缓冲区,你可以把模版缓冲区当做和深度缓冲区差不多的东东,每个像素有一个模板值,初始我们一般设为0,有API能对此进行操作.然后我们还需要知道在opengl中,我们把逆时针连接的面称为正面,另一面就是反面.

  Shadow Volumes简单来说,一般包含三次Pass.三次Pass都需要开启深度测试

  第一次我们正常渲染模型.打开深度缓冲区可写.不用打开模版测试.

            //第一次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算法.

  下面二图指示正面通过和反面通过的情形:

  前二图是在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 打不开或是出错请联系我.

时间: 2024-10-01 04:44:15

OpenGL 阴影之Shadow Mapping和Shadow Volumes的相关文章

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

实验平台:Win7,VS2010 先上结果截图(文章最后下载程序,解压后直接运行BIN文件夹下的EXE程序): 本文描述图形学的两个最常用的阴影技术之一,Shadow Mapping方法(另一种是Shadow Volumes方法).在讲解Shadow Mapping基本原理及其基本算法的OpenGL实现之后,将继续深入分析解决几个实际问题,包括如何处理全方向点光源.多个光源.平行光.最近还有可能写一篇Shadow Volumes的博文(目前已经将基本理论弄清楚了),在那里,将对Shadow Ma

Tutorial - Deferred Rendering Shadow Mapping 转

http://www.codinglabs.net/tutorial_opengl_deferred_rendering_shadow_mapping.aspx Tutorial - Deferred Rendering Shadow Mapping In this tutorial I will present the shadow mapping technique implemented in a deferred renderer. This tutorial will lean on

[OpenGL] shadow mapping(实时阴影映射)

source:原文地址 code:点击可以直接下载源代码 1978年,Lance Williams在其发表的论文<Casting curved shadows on curved surfaces>中提出了Shadow mapping算法,从那以后,该算法在离线渲染和实时渲染两个领域都得到了广泛的应用.皮尔斯动画工作室的Renderman渲染器.以及一些知名电影如<玩具总动员>都使用了shadow mapping技术. 在众多图形应用的阴影技术中,shadow mapping只是产

【学习笔记】3D图形核心基础精炼版-12:stage3D实战-动态阴影 shadow mapping 和范例工程4

目的: 物体投影在另一个物体身上,而另一个物体可能是平的,但大多数都是不平的多边形物体,这里考虑的是后者,这样可以适用于大多数场合的投影. 预览效果: 原理: 这里使用的是shadow mapping方式,其原理如下: 1.将场景的深度值预先渲染到 以光源位置为原点.光线发射方向为观察方向的投影坐标系中,形成深度纹理. 2.再次渲染场景的过程中,将每个片断(像素)变换到前述眼坐标系中,并缩放到[0,1]的范围内以便查询纹理. 3.以较暗的光照绘制场景 4.以当前片断在眼坐标中的S.T坐标查询深度

opengl 教程(23) shadow mapping (2)

原帖地址:http://ogldev.atspace.co.uk/www/tutorial24/tutorial24.html Background In the previous tutorial we learned the basic principle behind the shadow mapping technique and saw how to render the depth into a texture and later display it on the screen b

shadow mapping实现动态shadow实现记录

前段时间一直在弄一个室内场景,首先完成了render,效果还可以.然后给其加上shadow,使其更逼真.这里主要记录下在做的过程中遇到的问题. 1.是在导入场景的时候,由于场景比较大(200M)左右,所以在ios上加载这么大的场景会频繁的memorywarning,然后就会被系统kill掉.这个问题的解决方法是通过改变数据类型来达到压缩的目的.顶点的坐标double是没法改变的,如果改变会严重影响场景的准确度.这里主要是改变normal和 uv的类型,其实在正常的精度范围内,normal和 uv

shadow mapping实现动态shadow实现记录 【转】

http://blog.csdn.net/iaccepted/article/details/45826539 前段时间一直在弄一个室内场景,首先完成了render,效果还可以.然后给其加上shadow,使其更逼真.这里主要记录下在做的过程中遇到的问题. 1.是在导入场景的时候,由于场景比较大(200M)左右,所以在iOS上加载这么大的场景会频繁的memorywarning,然后就会被系统kill掉.这个问题的解决方法是通过改变数据类型来达到压缩的目的.顶点的坐标double是没法改变的,如果改

OpenGL阴影贴图详解

既然模拟出了光照,那么也少不了阴影,阴影的产生是因为距离光线较近的物体遮挡了距离较远的物体,导致被遮挡的物体接受的光照少于遮挡物的,因此阴影的产生与否与物体到光源的位置有关系,静态物体的阴影可以用光照贴图来模拟,而动态阴影要用阴影锥或者阴影贴图实现,阴影锥会引入许多额外的顶点为管线带来负担,目前比较流行的阴影模拟方法是用阴影贴图,它的好处在于只是用纹理存储物体的深度信息而并不会引入额外顶点. 要实现阴影贴图有以下几个步骤: 首先开辟一块纹理缓存以便之后保存世界的深度信息: //Create th

Shadow DOM获取Shadow host的内容

在Shadow DOM(二)中,介绍了与Shadow DOM相关的概念,包括Shadow host等等. 本文将重点介绍如何将Light DOM中的内容传到Shadow DOM中. 而在Shadow DOM 与HTML Templates一文的示例中可以看到Shadow host: <div class="host">Hello World!</div>的内容在该节点创建并附加Shadow Root后并没有在浏览器中得到渲染,也就是说Shadow host的内容