[图形学] 延迟贴花渲染技术 (A Deffered Decal Rendering Technique)

reference:《Game Engine Gems》 1

       渲染贴花是3D世界中动态生成细节的一种常用方法。贴花常用于制作子弹孔、血迹、轮胎痕迹以及其它相似的项作为游戏中发生的事件。它们也可以用于设计师使用位于墙上的污迹、破损效果的纹理来丰富场景。

        本文介绍了一种延迟贴花技术,它应用于使用延迟渲染生成场景的地方,如图20.1所示。该技术完全基于shader,而对CPU的依赖很小,不需要动态生成三角形,并且对于大多数3D引擎而言是一个直接的叠加效果。

        

        图20.1 应用在复杂几何体上的贴花

20.1 问题

         作为一个好的贴花系统,需要解决如下几个问题:

         1. 贴花系统需要和光照很好地结合起来。贴花不仅要能改变颜色,也需要能够改变法线,高光因子,以及其它表面参数,使它们和其它几何体变得无明显区别。

         2. 贴花应该能应用在所有表面上,包括静态和动态的。

         3. 贴花能够根据几何包围体进行合适的裁剪,甚至能够环绕角落。

         4. 对于和应用于碰撞检测的几何体有着很大差异的几何体,贴花系统需要能够生效。

        如果图形引擎具备延迟渲染的能力,那么第一个问题很容易解决。对于前向渲染而言,本章中的系统依然可以使用,但是需要做一些必要的修改。如果想要对延迟渲染有所了解,请参照[3,4,5,6]章节。

        如果我们能够在G-Buffer中占用一个8位通道,那么我们能够解决第二个问题。如果不能,我们至少可以使其在静态物体上运行。这在章节20.5中讨论。

        问题3是我们实际应用中可能遇到的问题的开始。贴花需要根据表面来得到它应用的位置,哪怕表面是高度棋盘格化的。一些现有的贴花技术会根据表面生成三角形网格,但是这在计算量上消耗很大。此外,原始网格在CPU上通常不可用,因为网格数据将加载到顶点着色器中,只能由GPU以合理的速度访问。

        问题4是一个我们面临的问题,因为当今的游戏通常会用比较复杂的网格进行渲染,并且使用细节更少的网格用于碰撞检测。难点在于光线投射返回的交点可能与渲染贴花的位置有很大的不同。

20.2 一般思路

       我们技术提供的基本解决方法是:使用一个应用到贴花包围体的特殊片元着色器,将贴花投影到表面。而不是在场景几何体上渲染一个新的贴花几何体。实现这一点所需的信息包括贴花的位置和方向、贴花的大小以及当前渲染视区的深度值纹理。此信息允许在顶点和片元着色器中进行所有计算。

        当渲染一个贴花的时候,我们很自然地会在“贴花空间”做这一工作,其中x,y轴位于贴花中心下的切线平面上,z轴与贴花中心的表面法向量平行。为了将数据移动到贴花的局部坐标系统,shader中需要乘以贴花变换矩阵的逆矩阵。

        20.1中所列出的代码首先在片元位置处从G-buffer中获取了深度,并且用其来重建片元的3D世界坐标。world到decal的常量就是我们使用的贴花渲染转换矩阵的逆。使用这一矩阵,我们可以将片元坐标转换为贴花的局部空间坐标。局部坐标可用于计算贴花纹理的采样坐标。在第一版中,我们只使用(x,y)坐标,它仅沿着本地z坐标投影到下面的几何体上,如图20.2所示。

        

        图20-2 在局部x-y平面上使用简单投影会使贴花沿着局部z轴方向延展。

        listing 20-1 : 此代码确定要渲染片段的贴花空间坐标,并将其转换为贴花的纹理坐标。RT-DEPTH纹理包含视区的深度值,worldToDecal常量是贴花从贴花空间到世界空间4x4矩阵转换的逆,recipDecalSize常量为1/s,其中s是场景中decal的大小。

        

// sample the depth at the current fragment
float pixelDepth = texture2DRect(RT_Depth, gl_FragCoord.xy).x;

// compute the fragment's world-space posiiton
vec3 worldPos = ComputeWorldSpacePosition(gl_FragCoord.xy, pixelDepth);

// transform into decal space
vec3 decalPos = worldToDecal * worldPos;

// use the xy position of the position for the texture lookup
vec2 texcoord = decalPos.xy * recipDecalSize * 0.5 + 0.5;

// fetch the decal texture color
gl_FragColor = texture2D(diffuseDecalTexture,texcoord);

20.3 几何体渲染

        现在我们有了一些计算基本纹理坐标的代码,我们必须考虑实际需要渲染的几何体类型。使用贴花shader渲染片段,就像透过一个窗口,我们可以看到我们的贴花。所以,我们可以只渲染和贴花的大小和位置相对应的表面齐平的四边形。但是,我们又希望贴花具有深度,因此我们改为渲染边界立方体,如图20-3所示。这允许我们从任何方向观察贴花,并且允许我们在之后添加一些环绕的特性。

        

    图20.3  我们可以渲染以贴花为中心的边界立方体,以确保从任何视角都能捕获到贴花的影响。

        对于包含大量贴花的场景做优化,通过硬件实例来渲染贴花会更有优势。因此,最好简单地渲染一个单位立方体,并将其转换为顶点着色器中的正确大小和位置,如list20.2所示。

        listing20.2 此顶点着色器将单位立方体缩放为贴花的实际大小,并将其转换为贴花的世界空间位置。

uniform float decalSize;
uniform mat4  decalToWorld;


// scale the unit cube and position it in world space
vec4 worldPos = decalToWorld * vec4(gl_Vertex.xyz * decalSize, gl_Vertex.w);

// output the position in homogeneous clip space
gl_Position = gl_ModelViewProjectionMatrix * worldPos;

        对于小的贴花,这种技术非常有效。但是,当一个立方体在启用背面剔除的情况下渲染,并且深度测试设置为GL_LESS时,相机进入立方体时贴花就会消失。该程序的一个解决方案是,剔除正面,并将深度测试设置为GL_GREATER。这样将只渲染其世界位置实际位于曲面后面的片段,但当许多片段被靠近摄像机的几何体遮挡时,会导致过度渲染。

        另一种解决方案是,找到最靠近摄像机的立方体,并在该深度处渲染一个与摄像机对齐的四边形,该四边形的大小足以包围整个立方体。如果最近的立方体角位于近平面的前面,则应在近平面处渲染全屏四边形。

20.4 淡出和环绕

        我们有一个基本的顶点和片元着色器,但是我们是沿着贴花的z轴无限投影的,会产生如图20.2所示的拉伸。有两种方法可以用来解决这个问题。更简单的方法是使用片元位置到贴花平面的距离作为淡出参数。这个距离在decalPos.z中可用,我们只需将其缩放到decal的大小,如list20.3所示。

        list 20.3 .片元位置的贴花空间z坐标的绝对值将缩放到贴花大小,并用作贴花颜色的淡出参数。和以前一样,recipDecalSize常量大小为1/s,其中s是场景中的贴花大小。

// compute the distance of the fragment to the decal's plane
float distance = abs(decalPos.z);

// scale the distance into the [0,1] range
// according to the size of the decal
float scaledDistance = max(distance * recipDecalSize * 2.0, 1.0);

// somehow use that scaled distance to fade out
// here : simple linear fade out
float fadeOut = 1.0 - scaledDistance;

vec4 diffuseColor = texture2D(diffuseDecalTexture, texcoord):

gl_FragColor = vec4(diffuseColor.rgb, diffuseolor.a * fadeOut);

        当贴花是平的而不是环绕几何图形时,此方法非常有用。用于淡出贴花的实际公式可以更改,以产生不同贴花的最佳外观,建议在游戏引擎内部可对此进行配置。

        处理拉伸问题的一个更有趣的方法是,使贴花环绕拐角,并跟随复杂曲面的曲率。这对于血液和其他液体尤其有用,因为如果这样的贴花覆盖了整个表面,而不受其曲率的影响,会更有说服力。

        这是很容易做到的。我们所需要的只是贴花中每个片元位置处的表面法向量。如果将该法向量旋转到贴花空间中,它的(x y)分量会给出曲面相对于贴花平面的梯度。我们可以使用这个梯度和片元到贴花平面的距离来修改纹理坐标。在没有相对坡度的区域,纹理查找保持不变,而在坡度较大的区域(例如,在拐角处),纹理坐标会根据到贴花平面的距离向外移动。List 20.4中说明了这一技术,结果如图20.4所示。

        

        图20.4 环绕角落的贴花基于表面法线计算,根据离贴花平面的距离淡出。

       Listing 20.4. 此代码演示了如何根据底部曲面的法向来调整纹理坐标,使得贴花围绕曲线和拐角。RT_NORMAL纹理包含在RGB通道编码的视区的法向量。

// get the world-space normal at the fragment position
vec3 worldNormal = texture2DRect(RT_Normal, gl_FragCoord.xy).xyz;

// rotate it into the local space of the decal
vec3 decalNormal = (worldToDecal * vec4(worldNormal.xyz, 0.0)).xyz;

vec2 texcoord = decalPos.xy;

// use the distance and gradient to modify the texture lookup
texcoord -= decalPos.z * decalNormal.xy;

// scale and center the texture coordinates
texcoord += vec2(decalSize);
texcoord *= recipDecalSize * 0.5;

gl_FragColor = texture2D(diffuseDecalTexture, texcoord);

        作为最后一笔,我们添加了颜色淡出,这取决于到贴花平面的距离。我们还需要考虑贴花平面法向量和下平面法向量之间的角度,否则,贴花会出现在较薄墙壁的背面。完整的shader例子可以在附带的CD中找到。

20.5 表面裁剪

         我们所描述的技术通过在视区空间中应用贴花来工作,并且不区分投影到的曲面。通过使用“投影和环绕”的方法,可以将贴花附加到任何类型的曲面,甚至附加到动画的角色上。然而,贴花永远不会被裁剪掉,这意味着贴在盒子上的贴花也会投射到盒子所在的地面上。如果盒子带着贴花移动,那么地面上的投影也会移动。

        这个问题有两种可能的解决方案。一种解决方案是将贴花渲染限制为仅由静态几何体覆盖的像素。在这种情况下,我们需要先渲染所有静态几何体,然后应用贴花,然后再渲染动态对象。

        第二个解决方案要求在G-Buffer中使用一个额外的通道来为渲染的每个不同对象保存一个贴花ID。在填充G-Buffer的渲染过程中,我们写入每个对象的贴花ID以及漫反射颜色、法线等,这样我们就有一个每像素的遮罩来标识每个像素属于哪个对象。当贴花应用于对象时,我们查找对象的ID并将其与贴花关联。当显示贴花时,我们将此ID与其它Uniform常量一起传递,并将其与从G-Buffer读取的ID进行比较。如果两者匹配,那么我们就知道像素属于贴花附着的对象,然后我们就正常的渲染。否则,我们丢弃片元,因为它将对象覆盖的像素集之外绘制。

        如果不需要区分静态几何图形,那么它们都可以共享相同的ID,比如0。所有的动态对象都应该有不同的ID,但这些ID不需要是唯一的。我们所要做的是使两个相邻的动态对象不可能具有相同的ID,然后就可以使用一个8位通道,简单地从范围内给动态对象随机的ID。

        这种对象管理也可以通过模板缓冲区来完成。但是,测试模板值将阻碍我们使用Instancing渲染贴花,因为我们无法将模板比较值作为uniform常量统一传递给着色器,并根据每个片元来修改模版测试。

20.6 局限性

        我们应该注意到一些限制。这种技术并不会创建起体积贴花,它只改进了二维投影,使它在许多情况下看起来更加真实。环绕特征使用曲面的法线来更改贴花内的纹理坐标。图20.4所示的结果比图20.2所示的结果要好很多,但是在垂直柱的边缘仍然有一个清晰可见的切口。为了防止这种伪影,我们需要使用真正的体积贴花。图20.5显示了应用于更复杂对象的贴花,在左侧图像中,对象使用面法线,在右侧图像中,对象使用平滑法线。

         在这两幅图像中,都未考虑法线映射,显然,下垫面的法线会对贴花的外观有着很大的影响。因此,具有强法线映射额曲面往往会扭曲贴花,有时会导致视觉瑕疵。不过,对于某些类型的贴花而言,这并不是一个问题,因为一个血迹不管在表面上多扭曲,它看起来仍然像一个血迹。

        

图20.5(左)基于法线的环绕(右)基于平滑插值的法线的环绕。

        关于性能:通过测试边界体积很容易剔除贴花,GPU可以通过实例批量呈现贴花。但是,你应该小心不要让许多贴花互相层叠,因为这样会消耗所有可用的片元渲染能力。可以尝试通过在一定时间后移除贴花,或一次检索屏幕上的贴花数量来防止这种情况。后一种方法允许我们保留世界上的许多贴花,只要它们在同一时间不可见即可。大多数游戏只需在固定时间后移除贴花即可清理场景,但在此过程中会降低沉浸式的质量。游戏Max Payne对在一个区域创建的贴花数量进行了限制,这有一个巨大的影响,即如果玩家回到之前所在的房间,所有的血迹和弹孔仍然存在。

20.7 额外功能

        由于我们是在任何光源计算之前将贴花渲染到G-Buffer中,我们不仅可以更改漫反射颜色,还可以替换(或修改)法线、光泽度以及其他数据。计算贴花的切线和次法线向量在顶点着色器中非常简单,不需要额外的顶点属性。有关详细信息,请参见附带CD上的shader代码。 

猜你喜欢

转载自blog.csdn.net/ZJU_fish1996/article/details/86762518
今日推荐