Decal渲染是一个引擎中重要的一部分,记忆中印象最深刻的就是以前CS中的弹痕与爆炸痕迹了。目前来说,Decal的实现方法也比较多,而且感觉还跟游戏类型有关,比如子弹乱飞的射击类FPS游戏中对贴花系统的要求就比较高,因为本来Decal的变化就比较丰富。一般来说贴花渲染主要有两种实现方法:
- Texture projection关于投影纹理的原理网上也有很多文章,基本就是需要对地形渲染两遍,一遍正常渲染,一遍用投影的纹理渲染,然后把这两个混合的结果融合。显然,如果场景中的贴花较多,且场景规模复杂的话,这种方法就会比较费。
- Mesh projection 这种方法应该来说使用比较多,包括早期Source中的Decal等使用的应该就是这种方法,其主要原理就是将Decal处理为附着于其所能贴到的几何表面上的几何网格,然后当做polygon list来渲染。当然这些网格也是经过修饰的(比如贴到一个桌子的边角上时,就需要根据桌角几何网格的分布来裁剪Decal所对应的网格)。关于该方法这里有一篇文章介绍得非常好:http://blog.wolfire.com/2009/06/how-to-project-decals/。另外,基于此种方法的改进类型也不少,比如Frosbite引擎中就利用Geometry Shader来生成类似该类型的Decal(Shadows and Decals in the Frostbite Engine)。
结合最近普遍应用的后处理渲染流程,于是自然而然地也就出现了Deferred Decal,这里一个最基本的条件就是需要有一个基于延迟渲染的系统才能使用这种方法。关于Deferred decal的实现原理也比较简单,过程如下:
- 在后处理渲染中完成正常的G-Buffer渲染后,即可得到对应的Normal-depth buffer,以及Diffuse Buffer,但是此时仍没有进行光照计算,这时就需要将Decal给渲染到对应的Diffuse buffer中。为了实现Decal,这里需要在G-Buffer后再增加一个Pass,并使用Diffse buffer作为Render target,并使用Decal shader进行渲染,计算并更新Decal所能够影响到的那些Pixel的Diffuse。
- 为了确定每个Decal所能够影响到的范围以便更新相应的Diffuse buffer,一般需要绘制出Decal所对应的一个包围几何体,这里可以是Sphere或Box(例如Volume decal中使用的就是Sphere,CryEnigne中使用的就是Box),当然,如果不计代价还可以使用更复杂的形体。
- 最后,关键的一步就是计算需要Decal的那些Pixel所对应的Decal uv,这里根据包围体的不同方法也不同。Sphere的映射方法比较简单,可以直接将Pixel的世界坐标转到局部球体坐标即可,再根据对应的Volume texture或2D texture再做一次映射,然后访问纹理即可。如果使用Box的话可能就稍麻烦些,这时需要知道Decal box所对应的一个局部坐标系转化。这个Decal坐标系由以下几个量来确定:Decal所贴的位置,Decal贴到处的Normal,Decal的水平方向,Decal的大小变换;然后由这三个基本量来构靠世界到Decal局部的变换矩阵:
- float3 gDecalScale;
- float3 gDecalPosition;
- float3 gDecalNormal;
- float3 gDecalLRVector;
- float3 zAxis = normalize(gDecalNormal);
- float3 xAxis = normalize(gDecalLRVector);
- float3 yAxis = normalize(cross(xAxis , zAxis));
- float4x4 decalMatrix =
- {
- {xAxis.x , yAxis.x , zAxis.x , 0.0f} ,
- {xAxis.y , yAxis.y , zAxis.y , 0.0f} ,
- {xAxis.z , yAxis.z , zAxis.z , 0.0f} ,
- {-dot(xAxis , gDecalPosition) , -dot(yAxis , gDecalPosition) , -dot(zAxis , gDecalPosition) , 1.0f}
- };
这里的变换矩阵其实也相当于在Decal的位置上放置一个Camera,即从Decal的角度来观察整个场景~_~float3 gDecalScale; float3 gDecalPosition; float3 gDecalNormal; float3 gDecalLRVector; float3 zAxis = normalize(gDecalNormal); float3 xAxis = normalize(gDecalLRVector); float3 yAxis = normalize(cross(xAxis , zAxis)); float4x4 decalMatrix = { {xAxis.x , yAxis.x , zAxis.x , 0.0f} , {xAxis.y , yAxis.y , zAxis.y , 0.0f} , {xAxis.z , yAxis.z , zAxis.z , 0.0f} , {-dot(xAxis , gDecalPosition) , -dot(yAxis , gDecalPosition) , -dot(zAxis , gDecalPosition) , 1.0f} };
- 得到变换矩阵之后即可对Pixel变换,然后在局部坐标中计算出对应的Decal UV就可以做相应的Decal纹理读取了。
比如将下面这个很有追求的Decal贴到Sponza场景中的效果如下
Enhanced effect with Bump:
此外,为了增强Decal的渲染效果可以使用带有Bump的Decal,不过这里要加入到整个渲染流程中的话可能会稍稍有些麻烦。为了使用Decal的Normal并在此上进行光照着色,就需要把Decal的Bump给附加到G-Buffer中的Normal上,这一步虽然与Decal texture的附加类似,但通常情况下Normal与Depth会在一个Buffer里边存储,而在进行Diffuse的decal pass时由于需要反求像素的世界位置,因而就要使用ND buffer,这样就不能在该pass里完成decal bump normal的合并。不过可以再建立一个跟Diffuse类似的中间Buffer,其只用来存储渲染Decal时生成的Normal值;然后在当前的Decal pass结束以后,再使用另外一个Pass,使用Normal Depth作为目标RT来合并原始Normal与Decal的Normal。
但是,这其中又涉及到另外一个变换:Decal normal由局部坐标到世界坐标的变换,这其实是相当于对应于decal space的一次tangent变换,需要对于上述的decalMatrix求逆转矩阵来做变换,而这一步的代价还是比较费的,因而此功能就需要谨慎使用。
Some Tips:
- 处理不同类型物体时的操作。在使用Defferd Decal的方法时,这个问题其实最为常见,也最需要解决。其实这里的不同类型较为广泛,比如说下述几何上不属于同一类物体(见下图),本来是想只将Decal贴在旗子上,但是其却也出现在了后面的墙上;另外,还在就是比如地面上的decal,但如是主角走过该处时,decal可能就同样会出现在角色身上。这个问题一般有两种解决方法:Stencil testing,Object index。前一种方法使用模板测试来标识不同的物体,相对来说其实现较为方便些,而第于二种方法可能需要改变G-Buffer的内容,在使用上可能还不如第一种方法,虽然其通用性更强。
- 关于Corner Wrap问题,这种情况的表现就像下图所示。该情况的产生是由于通常情况下只考虑Decal水平方向上的坐标投影变换来计算UV,而并没有考虑Decal normal方向上的影响。知道原因后这个问题比较容易解决,可以利用法向量的变化来计算Decal垂直方向上的UV分布就可以了。
- Decal texture采样寻址方式需要设置成CLAMP,否则就会出边重复的情况,视觉上的不正确。另外,也要注意Alpha的混合状态设置。
- 关于Camera进行Decal Box或Sphere内部的问题。由Defferred decal的实现原理可知,画出包围Decal的Box或Sphere其实是用来标记其所影响的Pixel以便更改diffuse,但这样就会有一个问题,那就是当Camera进入Box或Sphere内部后可能就会导致在视点处直接看到原始mesh,而不是box的mesh,这样肯定就不会进行Decal所对应的shader了,于是也就没法画上decal。这个问题也好解决,既然想增加对原始Mesh的遮挡,那么就可以将原始的Box或Sphere细分,即在其内部增加绘制的triangle以增加遮挡的机会,这样即使Camera进入其内部,仍然可以有内部的子部件来遮挡原始Mesh。不过简单起见可以只在Box内部再多画几个对角线上的triangle就可以应付大多数情况了。
- 对于Decal数量较多的情况,加入Decal Box或Sphere的实例化渲染以及对应Camera的可见性检测也是很有必要的。