阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。场景和物体的深度感因此能够得到极大提升,下图展示了有阴影和没有阴影的情况下的不同:

OpenGL java 影子 opengl加阴影_着色器


一般游戏中我们经常用阴影贴图的做法。

阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。

OpenGL java 影子 opengl加阴影_着色器_02


从上图我们可以看到,我们需要得到光线经过的物体片元的位置,和距离光源最近的片元作比较,只要距离大于最近的点 就说明它被遮挡,在阴影中。

那么我们的具体思路就是:

  1. 构建一个光源的视锥体,转换顶点到光源空间下
GLfloat near_plane = 1.0f, far_plane = 10.0f;
//投影矩阵
glm::mat4 lightProjection = glm::ortho(-10.0f, 10.0f, -10.0f, 10.0f, near_plane, far_plane);
//视图矩阵
glm::mat4 lightView = glm::lookAt(glm::vec3(4.0f, 4.0f, 4.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
glm::mat4 lightSpaceMatrix = lightProjection * lightView;
  1. 构建一个帧缓冲,只附加深度纹理附件,然后将光源空间下的深度贴图渲染至帧缓冲内(渲染迭代内的代码跟参考上章帧缓冲)
GLuint depthMapFBO;
    glGenFramebuffers(1,&depthMapFBO);
    //生成纹理 用来承载深度缓冲
    const GLuint SHADOW_WIDTH = 1024, SHADOW_HEIGHT = 1024;
    GLuint depthMap;
    glGenTextures(1,&depthMap);
    glBindTexture(GL_TEXTURE_2D,depthMap);
    //将纹理格式指定为GL_DEPTH_COMPONENT
    glTexImage2D(GL_TEXTURE_2D,0,GL_DEPTH_COMPONENT,SHADOW_WIDTH,SHADOW_HEIGHT,0,GL_DEPTH_COMPONENT,GL_FLOAT,NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    GLfloat borderColor[] = {1.0,1.0,1.0,1.0};
    glTexParameterfv(GL_TEXTURE_2D,GL_TEXTURE_BORDER_COLOR,borderColor);

    glBindFramebuffer(GL_FRAMEBUFFER,depthMapFBO);
    //把生成的深度纹理作为帧缓冲的深度缓冲
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_DEPTH_ATTACHMENT,GL_TEXTURE_2D,depthMap,0);
    //显式告诉OpenGL我们不使用任何颜色数据进行渲染
    glDrawBuffer(GL_NONE);
    glReadBuffer(GL_NONE);
    glBindFramebuffer(GL_FRAMEBUFFER,0);

3.将阴影贴图传进着色器,我们可以通过光空间下的片元坐标和阴影贴图中的采样进行比较,获取阴影系数,在阴影中是0.0,阴影外是1.0

//传入光空间下的片元坐标
float ShadowCalculation(vec4 fragPosLightPos){
    //透视除法 转化ndc坐标[-1,1]
    vec3 fragPosLight=fragPosLightPos.xyz/fragPosLightPos.w;
    //映射到[0,1]方便采样
    fragPosLight=fragPosLight*0.5+0.5;
    //对阴影贴图采样 获取光空间下 当前坐标最近深度
    float closestDepth=texture(depthMap,fragPosLight.xy).r;
    //光空间下 获取当前坐标的几个片元的深度(和最近深度比较)
    float currentDepth=fragPosLight.z;
    //如果该片元没有被光视锥体覆盖 就让它没有阴影
    if(currentDepth>1.0)
        return 1.0;
    //比较当前片元深度和最小深度,如果大于说明在阴影中
    return currentDepth>closestDepth?0.0:1.0;
}
  1. 片段着色器使用Blinn-Phong光照模型渲染场景。然后,diffuse和specular颜色会乘以这个阴影系数。由于阴影不会是全黑的(由于散射),我们把ambient分量从乘法中剔除。
vec3 result=ambient+(diffuse+specular)*shadow;

最终结果会是这个样子

OpenGL java 影子 opengl加阴影_光线方向_03


我们发现会有黑色条纹,这个是由于我们的阴影贴图分辨率不够导致多个片元会采样一个深度值,这多个片元距离光源的距离有远有近,他们都与同一个深度值比较,造成结果就是shadow值有的是1.0,有的是0.0。具体可以参考这篇博客,讲的很清楚。包括解决办法也有。

最终我们可以生成这样的阴影

OpenGL java 影子 opengl加阴影_贴图_04


地板上之所以会有黑暗的区域是因为我们构成光源视锥体没有覆盖那边区域,导致那边区域的片元深度大于1.0,我们可以判断当前深度大于1.0,让它强制返回1.0的深度系数就行

OpenGL java 影子 opengl加阴影_贴图_05