现代3D游戏经常会需要用到decal,子弹孔、脚印……传统的decal渲染从Quake 2时代开始就没怎么变过,通过画一个紧贴着物体的四边形来实现。问题一直很多,比如光照的不一致,z-fighting造成的闪烁,渲染状态的来回切换,等等。那么到了deferred框架中,这种情况会有所好转吗?
有趣的是,deferred框架内实现decal不但非常容易:100行之内的代码量,不到20分钟就能完成,而且非常稳定,完全没有上述各种缺点。更有趣的是,deferred的decal被至少三组不同的人都提出过完全相同的算法,都在讨论会出现的相同问题,只是被分别叫成了不同的名字。现在看看这三组:
- Crytek的Jan Krassnigg在2010年于Game Engine Gems 1里的一篇文章A Deferred Decal Rendering Technique提出了称为Deferred Decal的方法,同时他也在SIGGRAPH 2011的talk CryEngine 3: Reaching the Speed of Light中提到了一次。
- Avalanche Studios的Emil Persson在2011年的GPU Pro 2的文章中独立提出了Volume Decals,在他也给出了一个demo。
- Relic Entertainment的Pope Kim在SIGGRAPH 2012的一个talk中提出了Screen Space Decals。
既然这三种方法完全相同,我们其实只要讨论一种即可,统称为Deferred Decal。(顺便吐槽一下第三个,Screen Space Decals。作者不但是在看了前两个后提出了个毫无新意的东西,并且起了个奇烂无比的名字——很显然那东西不是screen space的。更恶心的是用了个SSD的缩写,嫌缩写混乱还不够多吗?他的ppt基本就是前两篇的问题,只是换成自己的图。遇到新问题直接就选择了避开,或者胡搞一个解法。)
Deferred Decal的基本思路就是,在G-Buffer建立之后,把每一个decal当作一个立方体迭加到G-Buffer上。对于G-Buffer中被立方体覆盖的像素,用decal的color和normal来替换掉。在PS中,只要
12345678910 | // 从eye坐标系转回decal立方体的坐标系 float4 decal_pos = mul(float4(pos_es, 1), inv_mv); // 忽略超过范围的像素 if (any( (decal_pos.xyz) > decal_pos.w)) { discard; } // 计算像素对应的decal纹理坐标 decal_pos /= decal_pos.w; texcoord = float2(decal_pos.x, -decal_pos.y) * 0.5f + 0.5f; |
由于操作的对象是整个G-Buffer,包括normal、diffuse、specular、shininess,所以这样的decal可以修改掉其中任何一个属性,并且在光照阶段不会增加任何开销。这也是传统的decal系统很难做到的。这里我贴了一个shininess很高的decal:
这种方法的最常见问题是,因为立方体的覆盖面积远远大于decal,有时候会在不该贴的地方贴上了拉伸严重的。解决方法是把decal立方体的normal和G-Buffer中的normal作比较,如果差别较大,就忽略掉。但因为写入的目标是G-Buffer,decal的PS就没法读取G-Buffer。所以我的方法是在PS里读取相邻点的depth,计算一个screen space normal(就好像里做的一样),把它和decal立方体的normal比较。下面比较了没加此处修正和加上后的区别:
修正前:
修正后:
另一个常见问题就是视点进入decal立方体后,由于culling的关系,decal消失。解决方法是在那种情况下改成画back face。
在deferred下实现decal就这么简单!