OpenGL 4.0 GLSL 用 shadow map 算法 生成阴影

shadow map 其实把场景渲染了两次,第一次是从灯光的角度(把相机放到灯光位置)渲染场景 ,然后存储渲染的深度信息到一张纹理上 即渲染一张shadow map(用FBO技术)。第二次再从观察者的角度来渲染。在第二次渲染时才渲染阴影,生成阴影的过程是这样的:对于每个片元的深度信息和第一次从灯光角度渲染的深度信息比较,如果当前深度值大于第一次渲染的深度值,则肯定有物体在当前片元和灯光之间,那么当前片元在阴影区。

如图:左边是灯光和其对应的透视截锥,右边是对应渲染出来的shadow map。在阴影图的灰度强度对应shadow map 中德深度值(越黑说明越近)



这个算法的关键之处在于片元三维坐标系的转换 这样可以在shadow map中寻找正确的纹理坐标。因为shadow map只是一个2D的纹理,我们需要在灯光的frustum中德点映射到从0-1之间。light‘view matrix 可以把world中的点转换到灯光坐标系下的点。light's projection matrix 可以 灯光坐标系下的点(within the light's frustum)转换到 其次裁剪坐标系(homogeneous clip coordinates.)

These are called clip coordinates because the built-in clipping functionality takes place when the position is defined in these coordinates. Points within the perspective (or orthographic) frustum are transformed by the projection matrix to the (homogeneous) space that is contained within a cube centered at the origin, with side length of 2. This space is called the canonical viewing volume. The term "homogeneous" means that these coordinates should not necessarily be considered to be true Cartesian positions until they are divided by their fourth coordinate. For full details about homogeneous coordinates, refer to your favorite textbook on computer graphics.


在裁剪坐标系中,点的x和y分量大体上可以作为访问纹理的坐标。而z分量存储了深度信息,我们可以和shadow map中的值作比较。 不过在用之前,我们有两件事要做:1. 我们需要一个偏转矩阵把他们映射到0-1(instead of -1 to 1)之间,2. 我们需要应用透视除法(perspective division)。

Of course, the x and y components also need to be biased between zero and one because that is the appropriate range for texture access.
We can use the following "bias" matrix to alter our clip coordinates.



 

顶点shader

#version 400
layout (location=0) in vec3 VertexPosition;
layout (location=1) in vec3 VertexNormal;

out vec3 Normal;
out vec3 Position;
out vec4 ShadowCoord;

uniform mat4 ModelViewMatrix;
uniform mat3 NormalMatrix;
uniform mat4 MVP;
uniform mat4 ShadowMatrix;

void main()
{
    Position = (ModelViewMatrix * vec4(VertexPosition,1.0)).xyz;
    Normal = normalize( NormalMatrix * VertexNormal );
    ShadowCoord = ShadowMatrix * vec4(VertexPosition,1.0);
    gl_Position = MVP * vec4(VertexPosition,1.0);
}

片元shader

#version 400

uniform struct LightInfo {
    vec4 Position;
    vec3 Intensity;
} Light;

uniform struct MaterialInfo {
    vec3 Ka;
    vec3 Kd;
    vec3 Ks;
    float Shininess;
} Material;

uniform sampler2DShadow ShadowMap;

in vec3 Position;
in vec3 Normal;
in vec4 ShadowCoord;

layout (location = 0) out vec4 FragColor;

vec3 phongModelDiffAndSpec()
{
    vec3 n = Normal;
    if( !gl_FrontFacing ) n = -n;
    vec3 s = normalize(vec3(Light.Position) - Position);
    vec3 v = normalize(-Position.xyz);
    vec3 r = reflect( -s, n );
    float sDotN = max( dot(s,n), 0.0 );
    vec3 diffuse = Light.Intensity * Material.Kd * sDotN;
    vec3 spec = vec3(0.0);
    if( sDotN > 0.0 )
        spec = Light.Intensity * Material.Ks *
            pow( max( dot(r,v), 0.0 ), Material.Shininess );

    return diffuse + spec;
}

subroutine void RenderPassType();
subroutine uniform RenderPassType RenderPass;

subroutine (RenderPassType)
void shadeWithShadow()
{
    vec3 ambient = Light.Intensity * Material.Ka;
    vec3 diffAndSpec = phongModelDiffAndSpec();

    float shadow = textureProj(ShadowMap, ShadowCoord);

    // If the fragment is in shadow, use ambient light only.
    FragColor = vec4(diffAndSpec * shadow + ambient, 1.0);//shadow 为1 则不在阴影区,为0 则在阴影区,只用环境光ambient

    // Gamma correct
    FragColor = pow( FragColor, vec4(1.0 / 2.2) );
}

subroutine (RenderPassType)
void recordDepth()
{
    // Do nothing, depth will be written automatically
}

void main() {
    // This will call either shadeWithShadow or recordDepth
    RenderPass();
}

设置FBO

void SceneShadowMap::setupFBO()
{
    GLfloat border[] = {1.0f, 0.0f,0.0f,0.0f };
    // The depth buffer texture
    GLuint depthTex;
    glGenTextures(1, &depthTex);
    glBindTexture(GL_TEXTURE_2D, depthTex);
    glTexStorage2D(GL_TEXTURE_2D, 1, GL_DEPTH_COMPONENT24, shadowMapWidth, shadowMapHeight);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_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);
    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, border);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LESS);    

    // Assign the depth buffer texture to texture channel 0
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, depthTex);

    // Create and set up the FBO
    glGenFramebuffers(1, &shadowFBO);
    glBindFramebuffer(GL_FRAMEBUFFER, shadowFBO);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT,
                           GL_TEXTURE_2D, depthTex, 0);

    GLenum drawBuffers[] = {GL_NONE};
    glDrawBuffers(1, drawBuffers);

    GLenum result = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    if( result == GL_FRAMEBUFFER_COMPLETE) {
        printf("Framebuffer is complete.\n");
    } else {
        printf("Framebuffer is not complete.\n");
    }

    glBindFramebuffer(GL_FRAMEBUFFER,0);
}

FBO只包含一个单一纹理附加到深度buffer上。GL_TEXTURE_WRAP_* 环绕模式设置成 GL_CLAMP_TO_BORDER.当一个片元处于shadow map 的外边(outside of the lights' frutum),那么这个片元的纹理坐标可能超出1或者小于0。在这种情况下,我们需要把这些点置于阴影之外(不在阴影中)。当用了GL_CLAMP_TO_BORDER,在这种情况下将返回边缘的值(the value that is returned from a texture lookup will the border value).默认的边缘值是(0,0,0,0)。当纹理包含深度分量,第一个分量被当作深度值。因此我们设置边缘颜色值为(1,0,0,0)(1对应最大深度值)。这样在比较时,所有处于light’s frutum 之外的片元,将不会在阴影中。


接下来两行

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LESS);

这样设置之后,访问纹理的结果将是一个比较的结果,而不是从纹理中取出的颜色值。纹理坐标ShadowCoord的第三个分量(r 分量)将会和和纹理坐标为(s,t)的值作比较。比较结果是一个float型值。GL_TEXTURE_COMPARE_FUNC 设置成GL_LESS((Other options include GL_LEQUAL,GL_ALWAYS, GL_GEQUAL, and so on.),意思是如果纹理坐标ShadowCoord的r值(第三分量)小于纹理坐标为(s,t) (ShadowCoord的前两个分量)的值结果将返回1.0


The next step is the key to the shadow mapping algorithm. We use the built-in texture access function textureProj to access the shadow map texture ShadowMap. Before using the texture coordinate to access the texture, the textureProj function will divide the first three coordinates of the texture coordinate by the fourth coordinate. Remember that this is exactly what is needed to convert the homogeneous position (ShadowCoord) to a true Cartesian position(笛卡尔坐标位置). 

After this perspective division, the textureProj function will use the result to access the texture. As this texture's type is sampler2DShadow, it is treated as texture containing depth values, and rather than returning a value from the texture, it returns the result of a comparison. The first two coordinates of ShadowCoord are used to access a depth value within the texture. That value is then compared against the value of the third component of ShadowCoord. When GL_NEAREST is the interpolation mode (as it is in our case) the result will be 1.0 or 0.0. As we set the comparison function to GL_LESS, this will return 1.0, if the value of the third component of ShadowCoord is less than the value within the depth texture at the sampled location. This result is then stored in the variable shadow.

结果如图:




shadow map 的抗锯齿问题:

如图:


之所以在边缘会产生锯齿效果,主要是因为shadow map 本身的分辨率是有限的,而在应用到一个高分辨率表面时就会出现这种情况。


在生产shadow map时只渲染背面。

When creating the shadow map, we only rendered back faces. This is because of the fact that if we were to render front faces, points on certain faces will have nearly the same depth as the shadow map's depth, which can cause fluctuations(起伏) between light and shadow across faces that should be completely lit. The following image shows an example of this effect.



为了避免这个问题,我们在生成shadow map时,只渲染back faces。当然,只有在你的mesh是完全封闭的时候才能这么做。否则,要用glPolygonOffset 来解决问题。不过,即使在只渲染背面的情况下也可能出现问题。如对于光源时背面,而对于相机是前面的情况。因此,更常见的是front-face culling 和  glPolygonOffset 一起用。


  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值