Shadow volume实现细节

来源:互联网 发布:apk制作软件安卓版 编辑:程序博客网 时间:2024/05/17 00:52

背景介绍

背景介绍:目前阴影算法较为流行的有两种,分别是shadow mappingshadow volume。前者实现起来相对简单,可以发挥现在GPU可编程流水线的能力,但是由于先天不足,shadow mapping在处理动态光源/物体的时候开销过大,经常作为一种静态场景中的廉价替代物。而 Shadow volume 的强项恰恰是 shadow mapping 的短处,像 DOOM3 这种大量运用动态光源,并且要对时刻都在运动中的物体投射阴影,shadow volume是现阶段唯一的选择。
但是这里并不介绍shadow mapping,网上有很多shadow mapping详细的教程,比如这里有详细教程,而且附带源码,如果感兴趣可以看看。
同样,网上也有很多shadow volume的教程,但是很多仅仅描述了核心的算法思想,具体实现细节并没有给出。对于初次接触OpenGL,或者接触不久的人来说,实现起来有点难度,所以我不仅仅描述核心的算法思想,而且还给出具体的实现细节。希望对于初学者有所帮助。

核心思想

我是根据网上这个教程Shadow Volume核心算法详解学习的,这个教程对于Shadow Volume的核心算法介绍的很详细。虽然很详细,但是为了加深我自己的理解,我决定也要写一下。
算法思想:就是光源照射到物体上,在物体的后面会形成一个阴影空间,如图所示

这里写图片描述

人物模型后面的蓝色空间,和机车后面的黑色空间,就是上面所说的阴影空间。然后再判断片段是否在这个阴影空间中,如果在这个阴影空间中,那么说明这个片段是有阴影的。
梳理一下,就是

  1. 生成阴影体
  2. 如何判断片段是否在阴影空间中(这部分是Shadow Volume的核心)。
  3. 如何将处于阴影体中的片段渲染成阴影。

第一个问题,如何生成阴影体

首先解决第一个问题如何生成阴影体?
如上图所示,光照射物体得到的阴影空间(阴影体),是沿着光照射的物体轮廓而产生的。建立的数学模型就是,物体轮廓的每一个点,沿着光线方向向下延伸到无限远处,然后轮廓所有点得到的延伸线组成一个封闭的空间,这就是所谓的阴影空间(阴影体)。
梳理一下,就是

  1. 寻找物体的轮廓
  2. 轮廓的点,沿着光线方向,向下无限延伸,并组合成一个封闭的空间

那么,下面咱们就解决这两个问题之一,生成一个阴影体。

1:寻找物体轮廓的方法。

要想得到物体的轮廓,我们必须要学习一个绘图方式GL_STRANGLE_ADJACENCY。这个绘图方式是OpenGL新增的。当使用GL_STRANGLE_ADJACENCY绘图的时候,传入着色器的数据不仅仅包括三角面片,还包括三角面片的邻接面。如下图所示。

这里写图片描述

中间实线的三角形是原三角面片,周围虚线的是三角面的邻接三角面。

那么这里存在一个问题。使用GL_STRANGLE方式绘图的时候,仅仅需要上图实线三角面的数据,而使用GL_STRANGLE_ADJACENCY方式绘图的时候,使用上图一个实线三角面和虚线三角面的数据。但是使用Assimp仅仅可以将obj数据导入成GL_STRANGLE格式的,那么如何将GL_STRANGLE换成GL_STRANGLE_ADJACENCY格式的呢?
那就需要将将要存入VAO中的数据扩大二倍,即原先3个顶点的,扩大到6个顶点,以便存放邻接信息,此外还需要更新索引,以便着色器每次可以将三角面片以及三角面片的邻接面片全部取出。
下面给出了实现方法:

myModel(const string& Filename, bool WithAdjacencies)    {        m_withAdjacencies = WithAdjacencies;        Clear();        glGenVertexArrays(1, &m_VAO);        glBindVertexArray(m_VAO);        // Create the buffers for the vertices attributes        glGenBuffers(ARRAY_SIZE_IN_ELEMENTS(m_Buffers), m_Buffers);        m_pScene = m_Importer.ReadFile(Filename.c_str(), aiProcess_Triangulate |            aiProcess_GenSmoothNormals |            aiProcess_FlipUVs |            aiProcess_JoinIdenticalVertices);        if (m_pScene) {            InitFromScene(m_pScene, Filename);        }        else {            printf("Error parsing '%s': '%s'\n", Filename.c_str(), m_Importer.GetErrorString());        }        glBindVertexArray(0);    }bool InitFromScene(const aiScene* pScene, const string& Filename)     {        m_Entries.resize(pScene->mNumMeshes);        m_Textures.resize(pScene->mNumMaterials);        vector<Vector3f> Positions;        vector<Vector3f> Normals;        vector<Vector2f> TexCoords;        vector<uint> Indices;        uint NumVertices = 0;        uint NumIndices = 0;        uint VerticesPerPrim = m_withAdjacencies ? 6 : 3;        // Count the number of vertices and indices        for (uint i = 0; i < m_Entries.size(); i++) {            m_Entries[i].MaterialIndex = pScene->mMeshes[i]->mMaterialIndex;            m_Entries[i].NumIndices = pScene->mMeshes[i]->mNumFaces * VerticesPerPrim;            m_Entries[i].BaseVertex = NumVertices;            m_Entries[i].BaseIndex = NumIndices;            NumVertices += pScene->mMeshes[i]->mNumVertices;            NumIndices += m_Entries[i].NumIndices;        }        // Reserve space in the vectors for the vertex attributes and indices        Positions.reserve(NumVertices);        Normals.reserve(NumVertices);        TexCoords.reserve(NumVertices);        Indices.reserve(NumIndices);        // Initialize the meshes in the scene one by one        for (uint i = 0; i < m_Entries.size(); i++) {            const aiMesh* paiMesh = pScene->mMeshes[i];            InitMesh(i, paiMesh, Positions, Normals, TexCoords, Indices);        }        // Generate and populate the buffers with vertex attributes and the indices        glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[POS_VB]);        glBufferData(GL_ARRAY_BUFFER, sizeof(Positions[0]) * Positions.size(), &Positions[0], GL_STATIC_DRAW);        glEnableVertexAttribArray(0);        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);        glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[TEXCOORD_VB]);        glBufferData(GL_ARRAY_BUFFER, sizeof(TexCoords[0]) * TexCoords.size(), &TexCoords[0], GL_STATIC_DRAW);        glEnableVertexAttribArray(1);        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 0, 0);        glBindBuffer(GL_ARRAY_BUFFER, m_Buffers[NORMAL_VB]);        glBufferData(GL_ARRAY_BUFFER, sizeof(Normals[0]) * Normals.size(), &Normals[0], GL_STATIC_DRAW);        glEnableVertexAttribArray(2);        glVertexAttribPointer(2, 3, GL_FLOAT, GL_FALSE, 0, 0);        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_Buffers[INDEX_BUFFER]);        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices[0]) * Indices.size(), &Indices[0], GL_STATIC_DRAW);        return GLCheckError();    }    void FindAdjacencies(const aiMesh* paiMesh, vector<unsigned int>& Indices)     {        // Step 1 - find the two triangles that share every edge        for (uint i = 0; i < paiMesh->mNumFaces; i++) {            const aiFace& face = paiMesh->mFaces[i];            Face Unique;            // If a position vector is duplicated in the VB we fetch the             // index of the first occurrence.            for (uint j = 0; j < 3; j++) {                uint Index = face.mIndices[j];                aiVector3D& v = paiMesh->mVertices[Index];                if (m_posMap.find(v) == m_posMap.end()) {                    m_posMap[v] = Index;                }                else {                    Index = m_posMap[v];                }                Unique.Indices[j] = Index;            }            m_uniqueFaces.push_back(Unique);            Edge e1(Unique.Indices[0], Unique.Indices[1]);            Edge e2(Unique.Indices[1], Unique.Indices[2]);            Edge e3(Unique.Indices[2], Unique.Indices[0]);            m_indexMap[e1].AddNeigbor(i);            m_indexMap[e2].AddNeigbor(i);            m_indexMap[e3].AddNeigbor(i);        }        // Step 2 - build the index buffer with the adjacency info        for (uint i = 0; i < paiMesh->mNumFaces; i++) {            const Face& face = m_uniqueFaces[i];            for (uint j = 0; j < 3; j++) {                Edge e(face.Indices[j], face.Indices[(j + 1) % 3]);                assert(m_indexMap.find(e) != m_indexMap.end());                Neighbors n = m_indexMap[e];                uint OtherTri = n.GetOther(i);                assert(OtherTri != -1);                const Face& OtherFace = m_uniqueFaces[OtherTri];                uint OppositeIndex = OtherFace.GetOppositeIndex(e);                Indices.push_back(face.Indices[j]);                Indices.push_back(OppositeIndex);            }        }    }    void InitMesh(uint MeshIndex,        const aiMesh* paiMesh,        vector<Vector3f>& Positions,        vector<Vector3f>& Normals,        vector<Vector2f>& TexCoords,        vector<uint>& Indices)    {        const aiVector3D Zero3D(0.0f, 0.0f, 0.0f);        // Populate the vertex attribute vectors        for (uint i = 0; i < paiMesh->mNumVertices; i++) {            const aiVector3D* pPos = &(paiMesh->mVertices[i]);            const aiVector3D* pNormal = &(paiMesh->mNormals[i]);            const aiVector3D* pTexCoord = paiMesh->HasTextureCoords(0) ? &(paiMesh->mTextureCoords[0][i]) : &Zero3D;            Positions.push_back(Vector3f(pPos->x, pPos->y, pPos->z));            Normals.push_back(Vector3f(pNormal->x, pNormal->y, pNormal->z));            TexCoords.push_back(Vector2f(pTexCoord->x, pTexCoord->y));        }        if (m_withAdjacencies) {            FindAdjacencies(paiMesh, Indices);        }        else {            // Populate the index buffer            for (uint i = 0; i < paiMesh->mNumFaces; i++) {                const aiFace& Face = paiMesh->mFaces[i];                assert(Face.mNumIndices == 3);                Indices.push_back(Face.mIndices[0]);                Indices.push_back(Face.mIndices[1]);                Indices.push_back(Face.mIndices[2]);            }        }    }

通过上面的代码可以将VAO中的GL_STRANGLE数据转变为GL_STRANGLE_ADJACENCY数据。
已经将VAO中的数据转化为GL_STRANGLE_ADJACENCY数据了,那么现在告诉你为什么转变成这种格式。怎么由这种格式可以判断出物体的轮廓。

这里写图片描述

再看这幅图,我们可以通过光线与法线的夹角来判断三角面的边属不属于轮廓!!!

主要思想是:判断当入射光线与024三角面的法线的点乘为正,入射光线与024三角面邻接三角面012三角面的法线的点乘 为负的时候,说明这两个三角面的公用边02为轮廓。

自然解释为,考虑自然界阴影轮廓,轮廓的一面为光照面,也就是光线能照射到三角面片上,光线和法线的点乘为正。轮廓的另一面为阴影面,也就是光源照射不到的地方,则光线和法线的点乘为负。
那么可以通过这个条件来判断某个边是否为轮廓。一般判断操作在几何着色器中进行,该部分代码如下所示。

void main(){    vec3 e1 = PosL[2] - PosL[0];    vec3 e2 = PosL[4] - PosL[0];    vec3 e3 = PosL[1] - PosL[0];    vec3 e4 = PosL[3] - PosL[2];    vec3 e5 = PosL[4] - PosL[2];    vec3 e6 = PosL[5] - PosL[0];    vec3 Normal = normalize(cross(e1,e2));    vec3 LightDir = normalize(gLightPos - PosL[0]);    // Handle only light facing triangles    if (dot(Normal, LightDir) > 0) {        Normal = cross(e3,e1);        //判断是否是物体轮廓边界,如果是则将该边界的两端顶点进行处理        //(沿着光线方向生成一个四边形面,并与其它轮廓生成的面组合成阴影体)。        if (dot(Normal, LightDir) <= 0) {              vec3 StartVertex = PosL[0];            vec3 EndVertex = PosL[2];            EmitQuad(StartVertex, EndVertex);        }        Normal = cross(e4,e5);        LightDir = gLightPos - PosL[2];        if (dot(Normal, LightDir) <= 0) {            vec3 StartVertex = PosL[2];            vec3 EndVertex = PosL[4];            EmitQuad(StartVertex, EndVertex);        }        Normal = cross(e2,e6);        LightDir = gLightPos - PosL[4];        if (dot(Normal, LightDir) <= 0) {            vec3 StartVertex = PosL[4];            vec3 EndVertex = PosL[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();        EndPrimitive();    }  }

2:轮廓的点,沿着光线方向,向下无限延伸,并组合成一个封闭的空间,形成一个阴影体。

通过上面算法,我们可以得到一个物体的轮廓边界,下面我们要通过这个轮廓来生成一个阴影空间(阴影体)。具体细节如下图所示,

这里写图片描述

V0和V1是检测出来的物体轮廓两端点的顶点,V3,V2分别是V0,V1沿光线方向向远处延伸的顶点,通过这样的无限延伸,我们能够确保阴影体能够将所有位于阴影中的对象都包含在内。当我们对组成轮廓的所有边都进行了上面的处理之后,阴影体的四周就创建成功了。但是阴影体需要时一个封闭的空间,所以还需要建立两个盖子,分别位于阴影体的上面和下面。此时阴影体就创建好了,这都是在几何着色器中创建的。
具体实现的
代码如下:

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();            }

第二个问题

此时我们已经解决了第一个问题如何生成阴影体。接下来解决第二个问题,如何判断片段是否在阴影体中。

这部分是Shadow Volume的核心,也是实现Shadow Volume的精髓所在,这就是z-fail算法。下面通过这幅图来介绍一下什么是z-fail算法,以及该算法是如何判断片段是否处于阴影中。

这里写图片描述

现在让我们看看如何利用模板缓存来产生阴影。首先我们像平常一样将场景对象(A, B,C 以及绿色的盒子)渲染到深度缓存中,完成之后我们就能得到场景中离相机最近的所有像素。之后我们遍历场景中的所有对象并为每个对象创建一个阴影体,上面的图例中只显示了绿色盒子的阴影体,但是在一个完整的程序中我们会为每个圆形对象都创建一个阴影体,因为他们也会产生阴影。阴影体的创建是根据检测出的轮廓(参考 39 课中的内容)并将其扩展到三维空间中。之后根据下面的一些简单规则将阴影体渲染到模板缓存中:

  • 在渲染阴影体反面时,如果深度测试失败则使模板缓存中的值加一;
  • 在渲染阴影体正面时,如果深度测试失败则是模板缓存中的值减一;
  • 在下面情况时我们不做任何处理:深度测试通过、模板测试失败。

上图所示。假设A,B,C的物体的初始模板值都是0。首先渲染阴影体反面时候,A和B物体位于阴影体背面的前方,所以可以通过深度测试,此时模板值A=0,B=0;C位于阴影体的后面,所以并没有通过深度测试,那么C片段的模板值C=1;然后再渲染阴影体正面,A处于阴影体正面的前方,所以通过深度测试,不作操作,此时模板值A=0,B和C位于阴影体正面的后方,所以深度测试失败,那么模板缓存中该片段的值减一。也就是B=-1,C=0;此时每个片段都被模板值标记,标记为非0的就是阴影区域。

这就是Z-fail的基本算法,当然这仅仅是最简单的情况,仅仅有一个阴影体。但是对于多个阴影体也是一样的,例如下图

这里写图片描述

已经知道思想,那么我们开始着手实现他。
首先我们需要渲染整个场景,对整个场景进行深度测试,用于下面的模板测试。开启深度测试,并打开深度缓冲写入功能,关闭颜色缓冲写入。

////将场景渲染到深度缓存里面        glDepthMask(GL_TRUE);        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);        //glDrawBuffer(GL_NONE);//glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);        depthShader.Use();        glUniformMatrix4fv(glGetUniformLocation(depthShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));        glUniformMatrix4fv(glGetUniformLocation(depthShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));        //Draw the loaded model        model = glm::translate(model, glm::vec3(0.0f, -3.5f, -1.0f));        model = glm::rotate(model, glm::radians(90.0f), glm::normalize(glm::vec3(1.0, 0.0, 0.0)));        model = glm::scale(model, glm::vec3(10.0, 10.0, 0.2));        glUniformMatrix4fv(glGetUniformLocation(depthShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));        //boxModel.Draw(depthShader, false);        MyModelMeshF.Render();        model = glm::mat4();        model = glm::translate(model, glm::vec3(0.0f, -1.5f, -1.0f));        model = glm::rotate(model, glm::radians((GLfloat)glfwGetTime() * 50.0f), glm::normalize(glm::vec3(0.0, 1.0, 0.0)));        glUniformMatrix4fv(glGetUniformLocation(depthShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));        //boxModel.Draw(depthShader, false);        MyModelMeshF.Render();

获得深度缓冲是必要的,因为深度缓存值也是Z-fail算法的基础,只有当深度测试没有通过,才对该片段的模板值进行加减。此时整个场景的深度值已经保存到深度缓冲中了,下面我们开始渲染阴影体。
开启模板测试,并且禁止深度值写入深度缓冲,关闭颜色缓冲写入。

//将场景渲染到模板缓存里面        glEnable(GL_STENCIL_TEST);        glDepthMask(GL_FALSE);        glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);        glEnable(GL_DEPTH_CLAMP);        glDisable(GL_CULL_FACE);        glStencilFunc(GL_ALWAYS, 0, 0xff);        glStencilOpSeparate(GL_BACK, GL_KEEP, GL_INCR_WRAP, GL_KEEP);        glStencilOpSeparate(GL_FRONT, GL_KEEP, GL_DECR_WRAP, GL_KEEP);        stencilShader.Use();        glUniformMatrix4fv(glGetUniformLocation(stencilShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));        glUniformMatrix4fv(glGetUniformLocation(stencilShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));        model = glm::mat4();        model = glm::translate(model, glm::vec3(0.0f, -1.5f, -1.0f));        model = glm::rotate(model, glm::radians((GLfloat)glfwGetTime() * 50.0f), glm::normalize(glm::vec3(0.0, 1.0, 0.0)));        //model = glm::translate(model, glm::vec3(0.0f, -2.5f, 0.0));        glUniformMatrix4fv(glGetUniformLocation(stencilShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));        glm::mat4 gWVP = projection * view * model;        glUniformMatrix4fv(glGetUniformLocation(stencilShader.Program, "gWVP"), 1, GL_FALSE, glm::value_ptr(gWVP));        //zhuziModel.Draw(stencilShader, true);        MyModelMeshT.Render();        glDisable(GL_DEPTH_CLAMP);        glEnable(GL_CULL_FACE);

此时已经执行了z-fail算法,更新了不同片段的模板值,处于阴影体内的模板值非0,未处于阴影体内的模板值为0。然后在进行模板测试,对于模板值非0的片段不进行着色。具体实现如下:

//渲染带阴影场景        glDepthMask(GL_TRUE);        glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);        glStencilFunc(GL_EQUAL, 0x0, 0xFF);        glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);        sceneShader.Use();        glUniformMatrix4fv(glGetUniformLocation(sceneShader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));        glUniformMatrix4fv(glGetUniformLocation(sceneShader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));        glUniform3fv(glGetUniformLocation(sceneShader.Program, "viewPos"), 1, &cameraPos[0]);        //glm::mat4 model;        model = glm::mat4();        model = glm::translate(model, glm::vec3(0.0f, -3.5f, -1.0f));        model = glm::rotate(model, glm::radians(90.0f), glm::normalize(glm::vec3(1.0, 0.0, 0.0)));        model = glm::scale(model, glm::vec3(10.0, 10.0, 0.2));        glUniformMatrix4fv(glGetUniformLocation(sceneShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));        //boxModel.Draw(sceneShader, false);        MyModelMeshF.Render();        model = glm::mat4();        model = glm::translate(model, glm::vec3(0.0f, -1.5f, -1.0f));        model = glm::rotate(model, glm::radians((GLfloat)glfwGetTime() * 50.0f), glm::normalize(glm::vec3(0.0, 1.0, 0.0)));        glUniformMatrix4fv(glGetUniformLocation(sceneShader.Program, "model"), 1, GL_FALSE, glm::value_ptr(model));        //boxModel.Draw(sceneShader, false);        MyModelMeshF.Render();        glDisable(GL_STENCIL_TEST);

shadow volume的算法基本上已经完成,具体效果如下:

这里写图片描述

再梳理一下整个文章脉络。

  • 将可以使用GL_STRANGLE绘图的数据转化为可以使用GL_STRSNGLE_ADJACENCY绘图的数据
  • 判断物体是每个三角面片的边是否物体轮廓
  • 根据上面所得的轮廓的两端顶点,沿着光线方向在无限远处生成一个顶点,这四个顶点组成一个四边形,所有四边形的几何,将组合成一个阴影空间(阴影体)。
  • 渲染整个场景到深度缓冲
  • 渲染阴影体到模板缓冲
  • 启动模板测试渲染场景,模板值为非零的不渲染

有源码,如果做不出来可以留言问我要。

原创粉丝点击