OGL(教程24)——阴影映射2

原文地址:http://ogldev.atspace.co.uk/www/tutorial24/tutorial24.html
项目地址:git@gitee.com:yichichunshui/ShadowMap2.git

背景知识:
前一小结学习阴影映射背后的基本原理。也学习了如何把深度缓冲渲染到一张纹理,然后紧接着从深度缓冲采样并绘制到屏幕。本节我们将学习如何使用阴影映射图绘制影子。

我们知道阴影映射是一个双通道技术。第一次渲染是从灯光位置渲场景,让我们回忆下第一次渲染时,位置向量z分量发生了什么。

  1. 传入到顶点着色器中的顶点数据是定义在本地空间的。
  2. 顶点着色器把顶本地空间的位置,转换到裁剪空间,然后传入到管线。
  3. 光栅化器执行透视除法(位置向量除以w分量)。这个把位置向量从裁剪空间转换到NDC空间。NDC空间下的坐标xyz分量映射到[-1,1]范围。超出这个范围的被裁剪掉。
  4. 光栅化器把xy坐标映射帧缓冲的维度(比如800600,或者是1024768)。这个坐标是屏幕空间坐标。
  5. 光栅化器接收三角形三个顶点的屏幕坐标,然后进行插值。对每个像素创建一个唯一的坐标。z值依然是[-1,1]区间,它也被插值,所有每个像素有自己的深度。
  6. 由于第一个通道我们关闭了颜色写入。深度测试依然执行。为了比较当前像素的z值,和之前深度缓冲中的z值。如果新的z值小于取出的深度值,那么深度值被更新。
    这上面的处理过程中,我们从光源的视角计算深度值,并存储起来。第二次渲染,我们从摄像机的角度渲染,得到另外一个深度。但是我们需要两个深度——一个是用于正确把三角形显示在屏幕上,另外一个是检测像素是否在阴影内。使用的技巧是,在阴影映射时候维护两个位置向量,连个wvp矩阵。一个wvp矩阵是从光源位置,另外一个是从摄像机位置。顶点着色器接收的是顶点本地位置,但是输出两个向量:
  7. gl_Position,是使用摄像机WVP矩阵变换得到的位置
    2.一个平的向量,是通过光源位置的WVP矩阵变换得到。
    第一个向量会传入上面的处理过程(得到NDC空间),这个会被正常的光栅化过程使用。第二个向量,会被光栅器插值,每个像素着色器调用会提供自己的值。所以现在,对于每个物体像素,我们同样有了从光源出看到的裁剪坐标。很有可能这从两个视角看到的物理像素是不同的,但是在三角形中的位置是相同的。所有剩下的就是根据裁剪坐标从阴影映射深度图中取出深度。取出之后,我们比较当前点的深度和取出的深度值,如果取出的深度值小于当前的深度,那么说明此点在阴影内。

那么如何在片段着色器中使用裁剪空间坐标取出深度值呢?我们分为两个步骤:

  1. 因为片段着色器接收的是裁剪空间坐标,光栅器没有执行透视除法。但是这个可以手动做下,就是每个分量处于w分量,就得到了NDC空间。
  2. 我们知道NDC空间的xy都在-1到1之间。在上面的第4步骤,光栅器把NDC空间映射到屏幕空间,然后使用它们存储深度。我们将要采样深度,所以我们需要贴图的坐标在[0,1]区间。如果把[-1,1]映射到[0,1],我们将会得到和纹理坐标,这个和阴影贴图有相同的位置。比如x在NDC中为0,贴图的宽度为800。0在NDC中需要映射到贴图的坐标为0.5(因为他是-1和1的中点)。所以纹理坐标为0.5的映射到400.
  3. 把x和y映射到NDC空间,如下:
u = 0.5 * X + 0.5
v = 0.5 * Y + 0.5

代码注释:

(lighting_technique.h:80)

class LightingTechnique : public Technique {
    public:
    ...	
        void SetLightWVP(const Matrix4f& LightWVP);
        void SetShadowMapTextureUnit(unsigned int TextureUnit);
    ...
    private:
        GLuint m_LightWVPLocation;
        GLuint m_shadowMapLocation;
...

光照技术需要一些新的属性。一个WVP矩阵,阿是从光源位置计算得出的,还有一个阴影映射图纹理单元。我们将会继续使用纹理单元0来作为常规的贴图,此贴图应用于物体,而纹理单元1用于阴影映射图。

(lighting.vs)

#version 330

layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;

uniform mat4 gWVP;
uniform mat4 gLightWVP;
uniform mat4 gWorld;

out vec4 LightSpacePos;
out vec2 TexCoord0;
out vec3 Normal0;
out vec3 WorldPos0;

void main()
{
    gl_Position = gWVP * vec4(Position, 1.0);
    LightSpacePos = gLightWVP * vec4(Position, 1.0);
    TexCoord0 = TexCoord;
    Normal0 = (gWorld * vec4(Normal, 0.0)).xyz;
    WorldPos0 = (gWorld * vec4(Position, 1.0)).xyz;
}

这个是LightingTechnique类中更新过的顶点着色器的代码,用加粗标记的是额外增加的代码。我们有一个WVP矩阵全局变量和一个4维的向量,它用来保存裁剪空间的坐标,它是通过WVP矩阵变换得到的。正如你看到的,

This is the updated vertex shader of the LightingTechnique class with the additions marked in bold text. We have an additional WVP matrix uniform variable and a 4-vector as output which contains the clip space coordinates calculated by transforming the position by the light WVP matrix. As you can see, in the vertex shader of the first pass the variable gWVP contained the same matrix as gLightWVP here and gl_Position there got the same value as LightSpacePos here. But since LightSpacePos is just a standard vector it does not get an automatic perspective division as gl_Position. We will do this manually in the fragment shader below.

(lighting.fs:58)

float CalcShadowFactor(vec4 LightSpacePos)
{
    vec3 ProjCoords = LightSpacePos.xyz / LightSpacePos.w;
    vec2 UVCoords;
    UVCoords.x = 0.5 * ProjCoords.x + 0.5;
    UVCoords.y = 0.5 * ProjCoords.y + 0.5;
    float z = 0.5 * ProjCoords.z + 0.5;
    float Depth = texture(gShadowMap, UVCoords).x;
    if (Depth < (z + 0.00001))
        return 0.5;
    else
        return 1.0;
}

片段着色器中使用上面的函数计算像素的影子系数。阴影系数在光照方程中是一个新的系数。我们只是简单的将当前的光照方程乘以这个因子,这个可以引起需要计算影子的像素有光源的衰减。这个函数接收的参数是经过差值的LightSpacePos向量,它是从顶点着色器传递过来的。第一步要执行的是透视除法——我们把xyz分量分别除以w分量。这样做就可以转换到NDC空间了。接着我们准备了2D坐标向量来作为贴图坐标,然后通过把LightSpacePos从NDC空间转换到贴图空间,此步参考的是背景知识的第3点。贴图坐标使用了从阴影贴图中采样深度值。这个深度值是场景中透视到同一个像素中所有点的离光源最近的点的深度值。我们把这个深度值和当前像素的深度值进行比较,如果这个深度值比当前像素的深度值小,说明,当前像素在阴影中,返回的系数是0.5,否则返回的影子系数为1.0(说明没有此像素不在阴影中)。Z坐标在NDC空间的范围在(-1,1)之间,有需要变换到(0,1)区间,因为我们需要在同一个坐标空间做比较。注意到我们误差值到当前像素深度值上。这个避免了处理浮点数精度导致的问题。

(lighting.fs:72)

vec4 CalcLightInternal(BaseLight Light, vec3 LightDirection, vec3 Normal, float ShadowFactor)
{
    ...
    return (AmbientColor + ShadowFactor * (DiffuseColor + SpecularColor));
}

处理灯光计算的函数变化很少。调用者需要传递阴影系数,漫反射颜色和镜面反射颜色需要通过这个因子进行调整。环境光不会被影响,因为环境光的定义是所有地方都有。

(lighting.fs:97)

vec4 CalcDirectionalLight(vec3 Normal)
{
    return CalcLightInternal(gDirectionalLight.Base, gDirectionalLight.Direction, Normal, 1.0);
}

我们的阴影映射实现目前只限于聚光灯。为了计算灯光的WVP矩阵,它需要位置和方向,这个是点光源和平行光所缺少的。点光源只需要位置,平行光只需要方向。我们会在后面的加入这些特性,但是目前我们队平行光只需要简单设置此系数为1。

(lighting.fs:102)

vec4 CalcPointLight(struct PointLight l, vec3 Normal, vec4 LightSpacePos)
{
    vec3 LightDirection = WorldPos0 - l.Position;
    float Distance = length(LightDirection);
    LightDirection = normalize(LightDirection);
    float ShadowFactor = CalcShadowFactor(LightSpacePos);

    vec4 Color = CalcLightInternal(l.Base, LightDirection, Normal, ShadowFactor);
    float Attenuation = l.Atten.Constant +
        l.Atten.Linear * Distance +
        l.Atten.Exp * Distance * Distance;

    return Color / Attenuation;
}

上面是点光源计算阴影的函数。在计算聚光灯的时候,也是同样调用计算点光源的函数。

(lighting.fs:117)

vec4 CalcSpotLight(struct SpotLight l, vec3 Normal, vec4 LightSpacePos)
{
    vec3 LightToPixel = normalize(WorldPos0 - l.Base.Position);
    float SpotFactor = dot(LightToPixel, l.Direction);

    if (SpotFactor > l.Cutoff) {
        vec4 Color = CalcPointLight(l.Base, Normal, LightSpacePos);
        return Color * (1.0 - (1.0 - SpotFactor) * 1.0/(1.0 - l.Cutoff));
    }
    else {
        return vec4(0,0,0,0);
    }
}
(lighting.fs:131)

void main()
{
    vec3 Normal = normalize(Normal0);
    vec4 TotalLight = CalcDirectionalLight(Normal);

    for (int i = 0 ; i < gNumPointLights ; i++) {
        TotalLight += CalcPointLight(gPointLights[i], Normal, LightSpacePos);
    }

    for (int i = 0 ; i < gNumSpotLights ; i++) {
        TotalLight += CalcSpotLight(gSpotLights[i], Normal, LightSpacePos);
    }

    vec4 SampledColor = texture2D(gSampler, TexCoord0.xy);
    FragColor = SampledColor * TotalLight;
}

最后,是片段着色器中的主函数。对于聚光灯和点光源我们使用的是相同的光源空间位置向量,尽管只有聚光灯支持。这个限制后面会修改。我们已经完成了所有灯光类的代码,现在来看看应用程序的代码。

(tutorial24.cpp:86)

m_pLightingEffect = new LightingTechnique();

if (!m_pLightingEffect->Init()) {
    printf("Error initializing the lighting technique\n");
    return false;
}

m_pLightingEffect->Enable();
m_pLightingEffect->SetSpotLights(1, &m_spotLight);
m_pLightingEffect->SetTextureUnit(0);
m_pLightingEffect->SetShadowMapTextureUnit(1);

这段从LightingTechnique 的Init方法开始,此方法只会执行一次,所以在此我们可以设置一些全局变量,他不会每帧都会变化。我们标准贴图单元属于网格的是0号单元。我们把1号分配给阴影贴图的。记住,shader程序别虚在变量设置之前变为enable状态,只要程序没有重新链接这些值都不会发生改变。这个很方便,因为它允许你在不同的shader程序之间切换,只需要关注一些全局变量即可。全局在开始启动的时候设置一次,之后都不会改变。

(tutorial24.cpp:129)

virtual void RenderSceneCB()
{
    m_pGameCamera->OnRender();
    m_scale += 0.05f;

    ShadowMapPass();
    RenderPass();

    glutSwapBuffers();
}

主循环中没有做任何的改变——使用相机进行渲染,然后缩放网格,然后是做影子映射通道渲染,然后是最后影子采样渲染。

(tutorial24.cpp:141)

virtual void ShadowMapPass()
{
    m_shadowMapFBO.BindForWriting();

    glClear(GL_DEPTH_BUFFER_BIT);

    m_pShadowMapEffect->Enable();

    Pipeline p;
    p.Scale(0.1f, 0.1f, 0.1f);
    p.Rotate(0.0f, m_scale, 0.0f);
    p.WorldPos(0.0f, 0.0f, 3.0f);
    p.SetCamera(m_spotLight.Position, m_spotLight.Direction, Vector3f(0.0f, 1.0f, 0.0f));
    p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);
    m_pShadowMapEffect->SetWVP(p.GetWVPTrans());
    m_pMesh->Render();

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
}

这个和之前一节的影子通道相同。唯一变化的是,我们能够对阴影映射进行开关操作,因为我们要在影子映射和光照技术之间做切换。注意,经过我们的场景包含一个网格还有一个四边形,称当地面。只有网格会被渲染到到阴影贴图,原因是地面不能释放阴影。这个在我们知道物体类型的时候可以做一些优化处理。

(tutorial24.cpp:168)

virtual void RenderPass()
{
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    m_pLightingEffect->Enable();

    m_pLightingEffect->SetEyeWorldPos(m_pGameCamera->GetPos()); 
    m_shadowMapFBO.BindForReading(GL_TEXTURE1);

    Pipeline p;
    p.SetPerspectiveProj(30.0f, WINDOW_WIDTH, WINDOW_HEIGHT, 1.0f, 50.0f);

    p.Scale(10.0f, 10.0f, 10.0f);
    p.WorldPos(0.0f, 0.0f, 1.0f);
    p.Rotate(90.0f, 0.0f, 0.0f);
    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
    m_pLightingEffect->SetWVP(p.GetWVPTrans());
    m_pLightingEffect->SetWorldMatrix(p.GetWorldTrans());
    p.SetCamera(m_spotLight.Position, m_spotLight.Direction, Vector3f(0.0f, 1.0f, 0.0f));
    m_pLightingEffect->SetLightWVP(p.GetWVPTrans());
    m_pGroundTex->Bind(GL_TEXTURE0);
    m_pQuad->Render();

    p.Scale(0.1f, 0.1f, 0.1f);
    p.Rotate(0.0f, m_scale, 0.0f);
    p.WorldPos(0.0f, 0.0f, 3.0f);
    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
    m_pLightingEffect->SetWVP(p.GetWVPTrans());
    m_pLightingEffect->SetWorldMatrix(p.GetWorldTrans());
    p.SetCamera(m_spotLight.Position, m_spotLight.Direction, Vector3f(0.0f, 1.0f, 0.0f));
    m_pLightingEffect->SetLightWVP(p.GetWVPTrans());
    m_pMesh->Render();
}

渲染通道和之前的章节中有同样的开始——首先是清除深度和颜色缓冲,用光照技术代替影子映射,然后绑定影子贴图帧缓冲对象,读取1号贴图纹理。接着渲染四边形,它称当地面,影子将会出现在其上面。它稍微被缩放,绕x轴旋转90度。注意WVP矩阵是如何更新的,它是在摄像机位置和光源位置之间做切换。由于四边形地面没有自身的贴图,我们手动的绑定一个贴图给它。网格也是类似处理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值