GraphicsLab Project之简易贴画系统(Decal System)

作者:i_dovelemon
日期:2018-08-28
来源:CSDN
主题:Projection Texture Mapping, Decal System


引言


游戏开发过程中有一个非常重要的功能:贴花(Decal)。这个功能指的是在多边形表面上绘制出其他图形,例如子弹击打到墙壁时的弹孔,英雄击打地面时产生的裂纹,车辆移动时的轨迹,游戏中玩家向墙上喷绘的logo等等。这样的功能,我们称之为:贴花(Decal)。


贴花实现的方法


经过一番调查,发现贴花的实现方法有以下四种可以使用:

  • 投影网格,就是根据计算在物体的表面实际添加一层网格用来绘制贴花
  • 投影贴图,根据投影贴图的功能实现,在绘制的过程中将贴图直接投影到物体表面上
  • 超大贴图,如果你的系统使用的是超大贴图,即整个场景使用一张贴图(id soft提出的算法),那么就可以简单的更改这张贴图实现贴花功能
  • 屏幕空间贴花,在屏幕空间实现贴花的功能

  • 方法 缺点
    投影网格 需要进行三角形级别的碰撞检测,由于是附着在物体表面之上的网格,存在z-fighting
    投影贴图 对于不同角度贴花,需要将场景多次分批次进行绘制,drawcall压力过大
    超大贴图 严重依赖系统基于mega texture的功能
    屏幕空间 严重依赖系统基于延迟渲染的渲染路径


    我的实现


    从上面不同方法的分析,我们可以从中选择合适的实现方法。现如今,大多成熟渲染系统都是基于延迟渲染的,所以他们大多使用的是基于屏幕空间的贴花系统,易于实现且功能强大。

    由于我的GLB Framework目前还是基于Forward的框架,所以此种方法暂时无法使用。

    而超大贴图的方法,依赖于系统是否使用了mega texture的功能,我的系统也没有使用这种功能,所以抛弃了。

    那么只剩下了投影网格和投影纹理这两种不同的方法。投影网格虽然有效,但是三角形级别的碰撞检测,会增加系统的复杂程度,同时z-fighting效果也不可避免。为了消除z-fighting,需要进行很多额外的消隐褪去的操作。所以我并不喜欢这种方法。

    那么就只剩下了投影纹理的方法。投影纹理能够很好的解决投影网格z-fighting的问题。这个方法,是在纹理采样级别,将贴花的图采样到物体表面上,属于完全的贴合在物体表面上。唯一的问题,不同角度,不同样式的贴花需要将场景绘制多次。一旦场景中贴花数量过多,就会导致draw call压力过大,这也是个很麻烦的问题。

    但是,由于我的游戏是一个俯视角的3D游戏。贴花主要集中在XZ平面之上,所以我使用了一个预处理,使得不需要增加额外的场景绘制就能够实现decal的功能。

    在讲解整个简易贴花系统的实现之前,我们先来了解下投影贴图的实现方法。


    投影贴图


    我们知道现在的3D光栅化流水线的基本操作如下:

  • 局部坐标系到世界坐标系(世界变换)
  • 世界坐标系到相机坐标系(相机变换)
  • 相机坐标系到NDC坐标系(投影变换)
  • NDC坐标系到屏幕坐标系(屏幕变换)

    也就是说,我们定义了世界和一个观察空间,然后将观察空间里面的世界部分变成了一张图片显示在了屏幕上。那么,也就是说屏幕上显示的这张图片和观察到的空间是一个对应的关系。明白了这点,我们就可以假设如果我们提前给出一张图片(比如贴花系统里面的贴花图),那么观察空间里面必然每一个点都对应了这张图上的某一个像素。根据这个关系,我们就能够实现贴花的功能。

    前面举例的时候,使用的观察空间是屏幕上玩家观察的观察空间。但是这个观察空间实际上是可以任意指定的,我们可以根据我们的需要来指定贴花投影的观察空间,这样这个观察空间里面对应的世界空间就能够通过一系列的计算得到我们指定的贴花图中的某一个像素,从而将贴图显示在世界空间里面。

  • 投影计算


    Nvidia有一篇paper详细的讲解了投影纹理以及投影计算的方法。我这里大概的讲解下这个流程:

  • 计算贴花观察空间的相机矩阵(View Matrix)和投影矩阵(Projection Matrix)
  • 正常绘制场景,计算顶点在贴花观察空间对应的贴花纹理坐标
  • 采样贴花贴图,和正常场景贴图进行混合

    如果你曾经做过Shadow Map相关的功能,那么你就会发现前面的操作流程和访问shadow map的方法一模一样。的确,shadow map也使用了投影纹理的相关技术。

  • 实现


    前面我们已经讲解过了如何将一张贴花投影到世界中去。从中你可以看出,不同的贴花观察空间需要计算不同的View Matrix和Projection Matrix,需要将场景绘制多次(或者传递一堆贴花贴图和矩阵到shader中去),严重影响效率。根据前面我们的描述,由于我的游戏采用的是俯视角,而贴花也主要集中在XZ平面之上,所以就有了这样的一个优化方案:

    我们提前将所有的贴花绘制到一张大图上去,然后将这张大图作为贴花使用上面的流程投影到世界空间中去。

    能这么做的基础就是前面提到的贴花都在XZ平面上。如果你的需求和我相似,那么你也可以使用这样的方法来实现。

    下面来看看代码的实现:

            // Create RenderTarget
            m_DecalRenderTarget = render::RenderTarget::Create(2048, 2048);
            m_DecalMap = render::texture::Texture::CreateFloat32Texture(2048, 2048);
            m_DecalRenderTarget->AttachColorTexture(render::COLORBUF_COLOR_ATTACHMENT0, m_DecalMap);


    首先创建了一个2048x2048的RenderTarget。这里的尺寸你可以根据实际需要选择你需要的尺寸,不过建议使用正方形的贴图可以方便后面计算投影矩阵。

            // Change Texture parameters
            glBindTexture(GL_TEXTURE_2D, reinterpret_cast(m_DecalMap->GetNativeTex()));
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);

    设置贴花贴图的采样方式。这里使用的是CLAMP_TO_BORDER的形式。也就是说,一旦采样的纹理坐标不再[0,1]之间,就会返回一个(0, 0, 0, 0)的颜色,方便后面进行贴花贴图与场景贴图的混合操作。

        void UpdateDecalPos() {
            static int32_t sFrame = 0;
            if (sFrame == 0) {
                // Create Decal position
                auto RandRange = [](int min, int max) {
                    return min + rand() % (max - min);
                };
    
                for (math::Vector& pos : m_DecalPos) {
                    pos = math::Vector(1.0f * RandRange(-20.0f, 20.0f), 0.0f, 1.0f * RandRange(-20.0f, 20.0f));
                }
    
                // Create Decal View Projection Matrix
                m_DecalViewProjM = CalculateDecalViewProjMatrix();
            }
            sFrame = sFrame + 1;
            if (sFrame > 100) sFrame = 0;
        }


    每隔100FPS,随机的改变下所有贴花的位置,然后根据这些位置计算最终的View Matrix和Projection Matrix。

        math::Matrix CalculateDecalViewProjMatrix() {
            math::Vector minPos(FLT_MAX, FLT_MAX, FLT_MAX);
            math::Vector maxPos(-FLT_MAX, -FLT_MAX, -FLT_MAX);
            for (math::Vector& pos : m_DecalPos) {
                if (pos.x < minPos.x) minPos.x = pos.x;
                if (pos.y < minPos.y) minPos.y = pos.y;
                if (pos.z < minPos.z) minPos.z = pos.z;
                if (pos.x > maxPos.x) maxPos.x = pos.x;
                if (pos.y > maxPos.y) maxPos.y = pos.y;
                if (pos.z > maxPos.z) maxPos.z = pos.z;
            }
    
            maxPos = maxPos + m_Decal->GetBoundBoxMax();
            minPos = minPos + m_Decal->GetBoundBoxMin();
    
            math::Matrix mat;
    
            // Size
            float width = (maxPos.x - minPos.x);
            float height = (maxPos.z - minPos.z);
            width = height = max(width, height);
            float depth = 10.0f + (maxPos.y - minPos.y);
    
            // Camera position in world space
            math::Vector pos = (maxPos + minPos) * 0.5f;
    
            // Target position in world space
            math::Vector look_at = math::Vector(0.0f, 1.0f, 0.01f);
            look_at.w = 0.0f;
            math::Vector target = pos + look_at;
    
            // Orthogonal projection matrix
            math::Matrix proj;
            proj.MakeOrthogonalMatrix(-width / 2.0f, width / 2.0f, - height / 2.0f, height / 2.0f, - depth / 2.0f, depth / 2.0f);
    
            // View matrix
            math::Matrix view;
            view.MakeViewMatrix(pos, target);
    
            // Shadow matrix
            mat.MakeIdentityMatrix();
            mat.Mul(proj);
            mat.Mul(view);
    
            return mat;
        }

    构造一个在XZ平面上足够大的观察空间,可以将所有的贴花贴图都观察到。观察的方向总是向-Y轴看(注意上面的代码为了防止除0异常,给观察方向Z加上了一点偏移)。这里定义的观察空间是一个长方体而不是平截头体,这就意味着我们需要使用正交投影矩阵。这个观察空间最重要的是XZ平面的大小,Y平面上的深度无关紧要,只要不是0即可,所以我在处理depth的时候,以10为基础。

    有了这些准备工作,接下来就将所有的贴花贴图绘制到前面我们创建的更大的render target之上:

        void DrawDecal() {
            // Setup render target
            render::Device::SetRenderTarget(m_DecalRenderTarget);
            render::Device::SetDrawColorBuffer(render::COLORBUF_COLOR_ATTACHMENT0);
    
            // Setup viewport
            render::Device::SetViewport(0, 0, 2048, 2048);
    
            // Clear
            render::Device::SetClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            render::Device::SetClearDepth(1.0f);
            render::Device::Clear(render::CLEAR_COLOR | render::CLEAR_DEPTH);
    
            // Setup shader
            render::Device::SetShader(m_DecalProgram);
            render::Device::SetShaderLayout(m_DecalProgram->GetShaderLayout());
    
            // Setup texture
            render::Device::ClearTexture();
            render::Device::SetTexture(0, render::texture::Mgr::GetTextureById(m_Decal->GetTexId(scene::Model::MT_ALBEDO)), 0);
    
            // Setup mesh
            render::Device::SetVertexLayout(render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexLayout());
            render::Device::SetVertexBuffer(render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexBuffer());
    
            // Setup render state
            render::Device::SetDepthTestEnable(true);
            render::Device::SetCullFaceEnable(false);
            render::Device::SetCullFaceMode(render::CULL_BACK);
    
            for (math::Vector& pos : m_DecalPos) {
                math::Matrix wvp = m_DecalViewProjM * math::Matrix::CreateTranslateMatrix(pos.x, pos.y, pos.z);
    
                // Setup uniform
                render::Device::SetUniformMatrix(m_DecalShaderWVPLoc, wvp);
                render::Device::SetUniformSampler2D(m_DecalShaderAlbedoLoc, 0);
    
                // Draw
                render::Device::Draw(render::PT_TRIANGLES, 0, render::mesh::Mgr::GetMeshById(m_Decal->GetMeshId())->GetVertexNum());
            }
    
            // Reset viewport
            render::Device::SetViewport(0, 0, app::Application::GetWindowWidth(), app::Application::GetWindowHeight());
    
            // Reset render target
            render::Device::SetRenderTarget(render::RenderTarget::DefaultRenderTarget());
        }


    在得到了这张合成之后的贴花图之后,我们就可以绘制正常场景,并且在场景里面访问这张贴图,然后进行合成。

        void DrawFloor() {
            // Setup shader
            render::Device::SetShader(m_ColorProgram);
            render::Device::SetShaderLayout(m_ColorProgram->GetShaderLayout());
    
            // Setup texture
            render::Device::ClearTexture();
            render::Device::SetTexture(0, m_FloorAlbedoMap, 0);
            render::Device::SetTexture(1, m_DecalMap, 1);
    
            // Setup mesh
            render::Device::SetVertexLayout(render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexLayout());
            render::Device::SetVertexBuffer(render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexBuffer());
    
            // Setup render state
            render::Device::SetDepthTestEnable(true);
            render::Device::SetCullFaceEnable(true);
            render::Device::SetCullFaceMode(render::CULL_BACK);
    
            // Setup uniform
            static float sRotX = 0.0f, sRotY = 0.0f;
            static float sPosX = 0.0f, sPosY = 0.0f, sPosZ = 0.0f;
            math::Matrix world;
            world.MakeIdentityMatrix();
            float mouseMoveX = Input::GetMouseMoveX();
            float mouseMoveY = Input::GetMouseMoveY();
            sRotX = sRotX + mouseMoveX * 0.1f;
            sRotY = sRotY + mouseMoveY * 0.1f;
            world.RotateY(sRotX);
            world.RotateX(sRotY);
    
            if (Input::IsKeyboardButtonPressed(BK_A)) {
                sPosX = sPosX + 0.1f;
            } else if (Input::IsKeyboardButtonPressed(BK_D)) {
                sPosX = sPosX - 0.1f;
            }
    
            if (Input::IsKeyboardButtonPressed(BK_Q)) {
                sPosY = sPosY + 0.1f;
            } else if (Input::IsKeyboardButtonPressed(BK_E)) {
                sPosY = sPosY - 0.1f;
            }
    
            if (Input::IsKeyboardButtonPressed(BK_W)) {
                sPosZ = sPosZ + 0.1f;
            } else if (Input::IsKeyboardButtonPressed(BK_S)) {
                sPosZ = sPosZ - 0.1f;
            }
            world.Translate(sPosX, sPosY, sPosZ);
    
            math::Matrix wvp;
            wvp.MakeIdentityMatrix();
            wvp = m_Proj * m_View * world;
            math::Matrix inv_trans_world = world;
            inv_trans_world.Inverse();
            inv_trans_world.Transpose();
    
            render::Device::SetUniformMatrix(m_ColorShaderWVPLoc, wvp);
            render::Device::SetUniformMatrix(m_ColorShaderDecalWVPLoc, m_DecalViewProjM);
            render::Device::SetUniformSampler2D(m_ColorShaderAlbedoLoc, 0);
            render::Device::SetUniformSampler2D(m_ColorShaderDecalMapLoc, 1);
    
            // Draw
            render::Device::Draw(render::PT_TRIANGLES, 0, render::mesh::Mgr::GetMeshById(m_Floor->GetMeshId())->GetVertexNum());
        }


    上面两个步骤分别使用了如下两个shader:

  • 将所有贴花贴图绘制到更大的rendertarget上去的shader:decal.vs, decal.fs

    这是一个很简单的shader如下:
  • #version 330
    
    in vec3 glb_attr_Pos;
    in vec2 glb_attr_TexCoord;
    
    uniform mat4 glb_WVP;
    
    out vec2 vs_TexCoord;
    
    void main() {
        gl_Position = (glb_WVP * vec4(glb_attr_Pos, 1.0));
        vs_TexCoord = glb_attr_TexCoord;
    }
    #version 330
    
    in vec2 vs_TexCoord;
    
    out vec4 oColor;
    
    uniform sampler2D glb_DecalTex;
    
    void main() {
        oColor = texture(glb_DecalTex, vs_TexCoord);
    }

  • 绘制场景时,访问合成的贴花贴图的shader:color.vs, color.fs

  • #version 330
    
    in vec3 glb_attr_Pos;
    in vec2 glb_attr_TexCoord;
    
    uniform mat4 glb_WVP;
    
    out vec2 vs_TexCoord;
    out vec4 vs_Vertex;
    
    void main() {
        gl_Position = (glb_WVP * vec4(glb_attr_Pos, 1.0));
        vs_TexCoord = glb_attr_TexCoord;
        vs_Vertex = vec4(glb_attr_Pos, 1.0);
    }
    #version 330
    
    in vec2 vs_TexCoord;
    in vec4 vs_Vertex;
    
    out vec3 oColor;
    
    uniform sampler2D glb_AlbedoTex;
    uniform sampler2D glb_DecalTex;
    uniform mat4 glb_DecalWVP;
    
    void main() {
        vec3 albedo = texture(glb_AlbedoTex, vs_TexCoord).xyz;
    
        vec4 decalTexcoord = glb_DecalWVP * vs_Vertex;
        decalTexcoord.xyz /= 2.0;
        decalTexcoord.xyz += 0.5;
        decalTexcoord.xyz /= decalTexcoord.w;
    
        vec4 decal = texture(glb_DecalTex, decalTexcoord.xy);
    
        oColor = albedo * (1.0 - decal.w) + decal.xyz * decal.w;
    }


    总结


    自此,一个简易的用于俯视角的贴花系统就成功了。当然,这里旨在给出基本的概念,可以在此基础之上进行更加复杂的扩展,实现你们想要的功能。完整的代码可以在这里找到。

    以下是本次demo的截图:


    GraphicsLab Project之简易贴画系统(Decal System)_第1张图片


    参考文献


    [1] Projective Texture Mapping
    [2] How to project decals
    [3] Drawing Stuff On Other Stuff With Deferred Screenspace Decals

    你可能感兴趣的:(3D引擎,游戏开发,Shader,GraphicsLab,Project,OpenGL,DirectX,图形试验室)