Stencil Shadow Volume技术讲解

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

最近给读者介绍关于阴影技术的实现原理,当试图为点光源生成阴影时,您需要一个方向矢量才能生成阴影贴图,而且由于点光源将光线投射到整个场景,所以很难获得这样的矢量。 虽然有办法解决这个问题,但它们有点复杂,其实影子贴图技术更适合聚光灯。 Stencil Shadow Volume是一种有趣的技术,可以为点光源的问题提供直接的解决方案。 这种技术是由William Bilodeau和Michael Songy于1998年发现的,由John Carmack在Doom 3引擎(2002)中普及。

实际上在我们的“延迟着色”中看到了这种技术的介绍, 通过延迟着色,我们需要一种阻止光线影响的方法, 我们仅在光照范围内的物体上处理照明 现在我们要做相反的事情 我们将创建一个阴影体积,并仅在其外部的物体上处理照明我们将使用模板缓冲区作为算法的关键组件 因此名称 -  Stencil Shadow Volume

阴影体积算法背后的想法是将光线减弱时创建的对象轮廓扩展到一个Volume中,然后使用一些简单的模版操作将该Volume映射到模板缓冲区中。 关键的想法是,当一个对象在Volume内(因此在阴影中)时,Volume前面的多边形会对对象的多边形进行深度测试,并且该Volume部的多边形将失效相同的测试,或者说不参与测试。

我们将根据称为Depth Fail的方法设置模板操作人们经常使用更直接的方法称为Depth Pass来开始描述阴影体积技术,但是当观看者本身位于影子内并且Depth Fail修复该问题时,我们已经跳过Depth Pass,直接去Depth Fail。 看看下面的图片:


我们在左下角有一个灯泡,一个绿色的物体(称为遮挡物体),由于光而投下阴影, 在这个场景中也渲染了三个圆形的对象。 对象B被遮蔽,而A&C不是。 红色箭头限定阴影体积的区域(线的虚线部分不是它的一部分)。

让我们看看我们如何利用模板缓冲区来获取阴影。 我们首先将实际对象(A,B,C和绿色框)渲染到深度缓冲区中。 当我们完成后,我们可以获得最接近的像素的深度。 然后我们一个接一个地遍历场景中的对象,并为每个对象创建一个阴影体积。 这里的示例仅显示绿色框的阴影体积,但在完整的应用程序中,我们还将为圆形对象创建Volume,因为它们投射自己的阴影。 阴影体积是通过检测它的轮廓来创建的(将在下篇博客中介绍)并将其扩展到无限远。 我们使用以下简单规则将该Volume渲染到模板缓冲区中:

1、如果在渲染阴影体积的背面多边形时深度测试失败,我们会增加模板缓冲区中的值。
2、如果在渲染阴影体积的前面多边形时深度测试失败,我们会减小模板缓冲区中的值。
3、在深度测试通过,模板测试失败情况下,我们什么都不做。

让我们看看使用上述方案的模板缓冲区会发生什么。 由物体A覆盖的Volume的正面和背面的三角形不能进行深度测试。 我们递增和递减模板缓冲区中由对象A覆盖的像素的值,这意味着它们保持为零。 在对象B的情况下,体积的前面三角形赢得深度测试,而背面的三角形失败。 因此,我们只会增加模板值。 覆盖对象C的体积三角形(正面和背面)赢得深度测试。 因此,模板值不会更新并保持为零。

请注意,到目前为止,我们还没有碰到色彩缓冲区。 当我们完成上述所有的操作后,我们再次使用标准的照明着色器渲染所有的对象,但是这次我们设置模板测试,使得只有模板值为零的像素才会被渲染。 这意味着只有对象A&C才能使其在屏幕上显示出来。

下面是一个更复杂的场景,场景中有两个遮挡物:



为了更容易地检测第二个遮挡物的阴影体积,它用红的箭头标记。 您可以按照模板缓冲区(由+1和-1标记)进行更改,并查看该算法在这种情况下也能正常工作。 从前一张照片的变化中可以看到A也是在阴影中。

让我们看看如何把这个知识付诸实践。 正如我们前面所说,我们需要渲染当我们扩展遮挡物的轮廓时创建的体积。 我们所需要做的就是将轮廓边缘延伸到一个体积中, 这是通过为每个轮廓边缘从GS发射四(或实际上四角形拓扑中的四个顶点)来完成的。 前两个顶点来自剪影边缘,当我们沿着从光照位置到顶点的向量将边缘顶点延伸到无穷大时,生成其他两个顶点。 通过延伸到无限远,我们确保体积捕获位于阴影路径中的所有物体。 这个四边形如下图所示:


当我们重复这个从所有轮廓边缘发射四边形的过程时,会创建一个体积。 够了吗? 当然不。 问题是这个体积看起来像一个没有盖子的截锥体。 由于我们的算法依赖于检查体积的前后三角形的深度测试,所以我们可能会遇到一个情况,即从眼睛到像素的矢量只能通过卷的正面或背面:


解决这个问题的方法是生成一个在两边封闭的体积。 这是通过创建一个正面和后面到体积(上图中的虚线)完成的。 创建前盖非常容易。 面向光的每个三角形都成为前盖的一部分。 虽然这可能不是最有效的解决方案,您可能会使用较少的三角形创建前盖,但绝对是最简单的解决方案, 后盖几乎是简单的。 我们只需要将面向三角形的光的顶点延伸到无限远(沿着从矢量到每个顶点)并反转它们的顺序(否则所得到的三角形将指向体积内)。

“无限”一词在这里已经提到过几次,我们现在需要确切地说明这是什么意思。 看看下面的图片:


我们看到的是从上面取出的截头锥体的图片, 灯泡发出一个穿过点'p'并继续无限远的光线。 换句话说,'p'扩展到无限远。 显然,在无穷远处,点p的位置是简单的(无穷大,无穷大,无穷大),但是我们不在乎。 我们需要找到一种光栅化阴影体积的三角形的方法,这意味着我们必须在投影平面上投影其顶点。 实际上这个投影平面是近平面。 虽然'p'沿着光矢量延伸到无穷远,但我们仍然可以在近平面上投射它。 这是通过从原点开始的虚线完成的,并在某处穿过光矢量。 我们要找到“Xp”,它是该矢量穿过近平面的点的X值。

我们将光矢量上的任何点描述为“p + vt”,其中“v”是从光源到点“p”的向量,“t”是从0到无穷大的标量。 从上图和三角相似之处可以看出:


其中'n'是近平面的Z值。 随着't'到无穷大,化简公式如下所示:


所以这就是我们在近平面上如何找到“无限远”的投影,根据上述我们只需要乘以矢量(Vx,Vy,Vz,0)(其中'V'是从光源到矢量的点p'的向量)来计算通过视图/投影矩阵并应用透视分割, 我们不会在这里证明,你可以自己尝试一下,看看结果。 所以底线是,只要我们需要光栅化一个包含一个顶点的三角形,该顶点沿一些向量扩展到无穷大,我们只需将该向量乘以视图/投影矩阵,同时向其添加一个零值的“w”分量 。 我们将在下面的GS中广泛使用该技术。

接下来给读者展示源代码:

glutInitDisplayMode(GLUT_DOUBLE|GLUT_RGBA|GLUT_DEPTH|GLUT_STENCIL);
在开始使用本博客之前,请确保按上述粗体显示的代码初始化FreeGLUT。 没有它,将在没有模板缓冲区的情况下创建帧缓冲区,不做任何工作。 我浪费了一段时间才意识到这是失误,所以请确保你添加这个。
virtual void RenderSceneCB()
{ 
    CalcFPS();

    m_scale += 0.1f;

    m_pGameCamera->OnRender();

    glDepthMask(GL_TRUE);

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

    RenderSceneIntoDepth();

    glEnable(GL_STENCIL_TEST);

    RenderShadowVolIntoStencil();

    RenderShadowedScene();

    glDisable(GL_STENCIL_TEST);

    RenderAmbientLight();

    RenderFPS();

    glutSwapBuffers();
}

主渲染循环功能执行算法的三个阶段。 首先,我们将整个场景渲染到深度缓冲区(不触摸彩色缓冲区)。 然后,我们将阴影体积渲染到模板缓冲区中,同时按照后台设置模板测试, 最后渲染场景本身,同时考虑到模板缓冲区中的值(即仅渲染模版值为零的像素)。

这种方法和阴影贴图之间的一个重要区别是,模板阴影体积方法中的阴影像素永远不会到达片段着色器。 当我们使用阴影贴图时,我们有机会计算阴影像素的环境照明。 我们在这里没有机会 因此,我们在模板测试之外添加环境通行证。

请注意,我们可以在调用glClear之前写入深度缓冲区。 没有它,深度缓冲区不会被清除。

void RenderSceneIntoDepth()
{
    glDrawBuffer(GL_NONE);

    m_nullTech.Enable();

    Pipeline p;

    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
    p.SetPerspectiveProj(m_persProjInfo); 

    m_boxOrientation.m_rotation = Vector3f(0, m_scale, 0);
    p.Orient(m_boxOrientation);
    m_nullTech.SetWVP(p.GetWVPTrans()); 
    m_box.Render(); 

    p.Orient(m_quadOrientation);
    m_nullTech.SetWVP(p.GetWVPTrans());
    m_quad.Render(); 
}
在这里,我们将整个场景渲染到深度缓冲区中,同时禁用对颜色缓冲区的写入。 我们必须这样做,因为在下一步我们渲染阴影体积,我们需要深度失败算法正确执行, 如果深度缓冲区仅部分更新,我们会得到不正确的结果。
void RenderShadowVolIntoStencil()
{
    glDepthMask(GL_FALSE);
    glEnable(GL_DEPTH_CLAMP); 
    glDisable(GL_CULL_FACE);

    // We need the stencil test to be enabled but we want it
    // to succeed always. Only the depth test matters.
    glStencilFunc(GL_ALWAYS, 0, 0xff);

    // Set the stencil test per the depth fail algorithm
    glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);
    glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP); 

    m_ShadowVolTech.Enable();

    m_ShadowVolTech.SetLightPos(m_pointLight.Position);

    // Render the occluder 
    Pipeline p;
    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());
    p.SetPerspectiveProj(m_persProjInfo); 
    m_boxOrientation.m_rotation = Vector3f(0, m_scale, 0);
    p.Orient(m_boxOrientation);
    m_ShadowVolTech.SetVP(p.GetVPTrans());
    m_ShadowVolTech.SetWorldMatrix(p.GetWorldTrans()); 
    m_box.Render(); 

    // Restore local stuff
    glDisable(GL_DEPTH_CLAMP);
    glEnable(GL_CULL_FACE); 
}
这是事情变得有趣的地方, 首先,我们禁用对深度缓冲区的写入(从上一步骤开始写入颜色已被禁用), 我们只是更新模板缓冲区, 我们启用深度堆,这将导致我们的投影到无穷远顶点(从远端)被钳制到最大深度值。 否则,远端将被简单地剪掉,我们还禁用后台剔除,因为我们的算法依赖于渲染体积的所有三角形。 然后,我们设置模板测试(在主渲染功能中已经启用)总是成功,并且根据深度故障算法设置正面和背面的模板操作。 之后,我们简单地设置着色器需要的所有内容并渲染遮挡物。
void RenderShadowedScene()
{
    glDrawBuffer(GL_BACK);

    // Draw only if the corresponding stencil value is zero
    glStencilFunc(GL_EQUAL, 0x0, 0xFF);

    // prevent update to the stencil buffer
    glStencilOpSeparate(GL_BACK, GL_KEEP, GL_KEEP, GL_KEEP);

    m_LightingTech.Enable();

    m_pointLight.AmbientIntensity = 0.0f;
    m_pointLight.DiffuseIntensity = 0.8f;

    m_LightingTech.SetPointLights(1, &m_pointLight);

    Pipeline p;
    p.SetPerspectiveProj(m_persProjInfo);
    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());

    m_boxOrientation.m_rotation = Vector3f(0, m_scale, 0);
    p.Orient(m_boxOrientation);
    m_LightingTech.SetWVP(p.GetWVPTrans());
    m_LightingTech.SetWorldMatrix(p.GetWorldTrans()); 
    m_box.Render();

    p.Orient(m_quadOrientation);
    m_LightingTech.SetWVP(p.GetWVPTrans());
    m_LightingTech.SetWorldMatrix(p.GetWorldTrans());
    m_pGroundTex->Bind(COLOR_TEXTURE_UNIT);
    m_quad.Render(); 
}

我们现在可以使用更新的模板缓冲区, 基于我们的算法,只有当像素的模板值完全为零时,才将渲染设置为成功。 此外,我们还通过将模板测试操作设置为GL_KEEP来阻止对模板缓冲区的更新。 就是这样! 现在我们可以使用标准的照明着色器渲染场景。 只要记住在启动之前先写入彩色缓冲区...

void RenderAmbientLight()
{
    glEnable(GL_BLEND);
    glBlendEquation(GL_FUNC_ADD);
    glBlendFunc(GL_ONE, GL_ONE);

    m_LightingTech.Enable();

    m_pointLight.AmbientIntensity = 0.2f;
    m_pointLight.DiffuseIntensity = 0.0f;

    m_LightingTech.SetPointLights(1, &m_pointLight);

    m_pGroundTex->Bind(GL_TEXTURE0);

    Pipeline p;
    p.SetPerspectiveProj(m_persProjInfo);
    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp());

    m_boxOrientation.m_rotation = Vector3f(0, m_scale, 0);
    p.Orient(m_boxOrientation);
    m_LightingTech.SetWVP(p.GetWVPTrans());
    m_LightingTech.SetWorldMatrix(p.GetWorldTrans()); 
    m_box.Render();

    p.Orient(m_quadOrientation);
    m_LightingTech.SetWVP(p.GetWVPTrans());
    m_LightingTech.SetWorldMatrix(p.GetWorldTrans());
    m_pGroundTex->Bind(COLOR_TEXTURE_UNIT);
    m_quad.Render(); 

    glDisable(GL_BLEND);
}
环境通道帮助我们避免通过模板测试丢掉的完全黑色像素, 在现实生活中,我们通常看不到这样的极端阴影,所以我们向所有像素添加一点环境光。 这通过在模板测试的边界之外进行另外的照明通行来完成。 在这里要注意的几件事情:我们消除漫反射强度(因为那个受影子影响),我们启用混合(合并上一遍的结果)。 现在让我们来看看阴影体积技术的着色器。

(shadow_volume.vs)
#version 330

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

out vec3 PosL;

void main() 
{ 
    PosL = Position;
}

在VS中,我们简单地按照原样转发位置(在本地空间中), 整个算法在GS中实现。

(shadow_volume.gs)
#version 330

layout (triangles_adjacency) in; // six vertices in
layout (triangle_strip, max_vertices = 18) out;

in vec3 PosL[]; // an array of 6 vertices (triangle with adjacency)

uniform vec3 gLightPos;
uniform mat4 gWVP;

float EPSILON = 0.0001;

// Emit a quad using a triangle strip
void EmitQuad(vec3 StartVertex, vec3 EndVertex)
{
    // Vertex #1: the starting vertex (just a tiny bit below the original edge)
    vec3 LightDir = normalize(StartVertex - gLightPos); 
    gl_Position = gWVP * vec4((StartVertex + LightDir * EPSILON), 1.0);
    EmitVertex();

    // Vertex #2: the starting vertex projected to infinity
    gl_Position = gWVP * vec4(LightDir, 0.0);
    EmitVertex();

    // Vertex #3: the ending vertex (just a tiny bit below the original edge)
    LightDir = normalize(EndVertex - gLightPos);
    gl_Position = gWVP * vec4((EndVertex + LightDir * EPSILON), 1.0);
    EmitVertex();

    // Vertex #4: the ending vertex projected to infinity
    gl_Position = gWVP * vec4(LightDir , 0.0);
    EmitVertex();

    EndPrimitive(); 
}


void main()
{
    vec3 e1 = WorldPos[2] - WorldPos[0];
    vec3 e2 = WorldPos[4] - WorldPos[0];
    vec3 e3 = WorldPos[1] - WorldPos[0];
    vec3 e4 = WorldPos[3] - WorldPos[2];
    vec3 e5 = WorldPos[4] - WorldPos[2];
    vec3 e6 = WorldPos[5] - WorldPos[0];

    vec3 Normal = cross(e1,e2);
    vec3 LightDir = gLightPos - WorldPos[0];

    // Handle only light facing triangles
    if (dot(Normal, LightDir) > 0) {

        Normal = cross(e3,e1);

        if (dot(Normal, LightDir) <= 0) {
            vec3 StartVertex = WorldPos[0];
            vec3 EndVertex = WorldPos[2];
            EmitQuad(StartVertex, EndVertex);
        }

        Normal = cross(e4,e5);
        LightDir = gLightPos - WorldPos[2];

        if (dot(Normal, LightDir) <= 0) {
            vec3 StartVertex = WorldPos[2];
            vec3 EndVertex = WorldPos[4];
            EmitQuad(StartVertex, EndVertex);
        }

        Normal = cross(e2,e6);
        LightDir = gLightPos - WorldPos[4];

        if (dot(Normal, LightDir) <= 0) {
            vec3 StartVertex = WorldPos[4];
            vec3 EndVertex = WorldPos[0];
            EmitQuad(StartVertex, EndVertex);
        }

        // render the front cap
        LightDir = (normalize(PosL[0] - gLightPos));
        gl_Position = gWVP * vec4((PosL[0] + LightDir * EPSILON), 1.0);
        EmitVertex();

        LightDir = (normalize(PosL[2] - gLightPos));
        gl_Position = gWVP * vec4((PosL[2] + LightDir * EPSILON), 1.0);
        EmitVertex();

        LightDir = (normalize(PosL[4] - gLightPos));
        gl_Position = gWVP * vec4((PosL[4] + LightDir * EPSILON), 1.0);
        EmitVertex();
        EndPrimitive();

        // render the back cap
        LightDir = PosL[0] - gLightPos;
        gl_Position = gWVP * vec4(LightDir, 0.0);
        EmitVertex();

        LightDir = PosL[4] - gLightPos;
        gl_Position = gWVP * vec4(LightDir, 0.0);
        EmitVertex();

        LightDir = PosL[2] - gLightPos;
        gl_Position = gWVP * vec4(LightDir, 0.0);
        EmitVertex();
    }
}

在这个意义上,GS只是像轮廓着色器一样开始,我们只关心三面体的光线。 当我们检测到轮廓边缘时,我们将四边形从它延伸到无限远。 请记住,原始三角形的顶点索引为0,2和4,相邻顶点为1,3,5。 在我们照顾四边形之后,我们会排出前盖和后盖。 请注意,对于前盖,我们不使用原来的三角形。 相反,我们沿着光矢量移动很少的数量(我们通过归一化光矢量并将其乘以一个小的epsilon来实现)。 原因是由于浮点错误,我们可能会遇到奇怪的表现,其中体积隐藏了前盖,将盖子从体积中移开一点就可以解决这个问题。

对于后盖,我们简单地将原始顶点投影到无穷远沿光矢量并以相反的顺序发射。
为了从边缘发射四边形,我们沿着光线方向将两个顶点投影到无限远,并生成三角形条。 请注意,原始顶点沿着光矢量移动非常小的数量,以匹配前盖。
至关重要的是我们正确设置GS的最大输出顶点(参见上面的“max_vertices”)。 前盖有3个顶点,后盖有3个顶点,每个轮廓边缘有4个。 如果把这个值设置为10,并且有非常奇怪的表现, 确保你不犯同样的错误...



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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海洋_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值