这里来学习下Inside团队在GDC 2016上的技术分享,主要包含如下几个方面的工作:
- 体积雾
- HDR Bloom
- Color Banding的Dither解决方案
- 贴花
- 水体渲染
照例对全文的要点做一个总结:
- Unity开发,延迟渲染管线
- Static/Stationary Light做了Shadow Cache
- 环境反射贴图是预计算(没说离线)得到的,并在多帧之内复用
- 针对Glow分为两个Pass完成,其中LDR Glow用的是Buttery Lens方案,针对光照的HDR Glow则是另一个(模糊半径窄,只用于光源物体),两者并没能结合到一个pass中完成
- Glow的上采样跟下采样的算法,包括样本数以及样本分布,都是不一样的(目前推测是下采样要求高,所以样本数跟采样点分布要求也高,否则会导致效果瑕疵)
- 通过为贴图的三个通道使用不同的offset来实现色差效果,也就是需要三个采样
- 体积雾通过ray marching实现,通过起始步长的随机抖动(基于蓝噪声,很多随机效果都建议基于蓝噪声来)、采样点聚焦在light frustum跟view frustum相交的区域、半分辨率渲染(噪声模糊上采样)、结合TAA、Deinterleaved Texture Sample方案来实现性能的提升。
- 其他用到了基于噪声的随机性来提升性能的方案有DOF(不同位置的模糊半径不同)、软影(spiral disk sampling pattern)
- 针对banding问题,就用了前面文章介绍的Dither方案来做消除
- 借助延迟渲染管线,做了较多的自定义光照效果,比如模拟GI的Bounced Light、比如模拟AO的点光、box光源,比如模拟阴影的Decal
- SSR效果是局部执行的,限定了tracing像素的范围,避免了全局的tracing消耗;同时针对各种corner case的解决方案做了分享
- 针对水体的渲染,水上跟水下部分采用了不同的渲染顺序来保证效果的正确
- 针对特效如烟雾、火焰等,采用了distortion来模拟更真实的动态效果,同时针对火焰用了gradient贴图来实现染色、通过重复+随机的思路来兼顾多样性与表现的平滑、通过带gradient的渐变来实现切换的自然等小trick来提升整体的表现,颇有可圈可点之处。
- Lens Flare不是通过后处理完成的,而是通过一个可以读取深度的sprite实现的,可以较好的降低实现成本
- 雨效则是采用顶点动画方案来跳过粒子生成、overdraw消耗,同时避免了后处理的视差缺失问题
- 水体的交互波纹效果主要通过法线贴图的改动来实现,洪水效果则通过地表的贴花、UV动画、水花特效等方式实现
采用Unity开发
全文大纲如上
Trailer: http://playdead.com/inside/
这里给出了产品的基本状态:
- 2.5D的side-scrolling,固定FOV,可以非常精确的控制呈现给玩家的内容
- 团队的美术都没啥技术上的经验,在sprite制作上造诣颇深
- 艺术表现与画面细节有较强的关联
同时上面还给了技术指标
采用的是延迟渲染管线,在绘制透明物件之前,会做一次framebuffer的copy,此外static light的shadowmap会在多帧中复用。
反射贴图是在使用之前预计算的。
项目中的大量效果都是通过雾效+剪影实现的
下面用一组图品来介绍下有雾效跟没有雾效的区别,上图是没有雾效的
添加简单的线性雾,这里限定了雾效的最大值,从而可以允许类似headlight之类的白色物件能够显示出来(这里没展示)
加了glare,也就是大气散射效果(近似)
Glow效果是通过传统的Buttery lens LDR方案实现的,通过下采样+多次模糊处理(每一次滤波半径都比之前一次大)来实现,这里给了两个参考链接:
- http://www.chrisoat.com/papers/Oat-SteerableStreakFilter.pdf
- www.daionet.gr.jp/~masa/archives/GDC2003_DSTEAL.ppt
项目还需要一个针对HDR的glow pass(光照部分),曾经尝试过将HDR glow跟LDR glow结合到一个pass来完成,但实践发现两者之间会有比较强的干涉,问题没能很好解决,最终还是采用了两个单独pass来解决的。
HDR的glow,模糊半径相对窄,且只应用于特定的物件:自发光材质
如果bloom是基于某个空间(LDR或HDR)的intensity计算的,那bloom应用的时候也应该使用相同空间的intensity,否则就会看起来怪怪的,简单来说就是bloom的intensity跟自发光的intensity不匹配,最好的解决方案就是在HDR空间中计算,也就在HDR空间中进行应用。
bloom的模糊有两个选择:
- 下采样部分模糊,采用13tap(每次模糊做13次采样)的模糊处理
- 上采样部分模糊,采用9tap的模糊处理
关于这个,还给了两个参考文献:
- Siggraph 2014 - Next Generation Post-Processing in Call of Duty
- The Technology Behind the “Unreal Engine 4 Elemental demo"
下采样的13个样本的分布与权重:
- 通过tent分布来逼近高斯
- 同一个像素会被多次采样
上采样模糊的9个样本则是按照三角形来分布,这时候的像素就不会被多次采样了,权重则是美术同学根据表现手动编辑给出的。
尝试将下采样的13个样本拟合到9个中,可以通过脚本来实现这个过程,最大的加权误差是0.08,效果上来看基本上差不多。
不过由于这里对双线性插值有非常强的依赖,如果我们下采样的分辨率并不是精准的一半,就会有严重的问题,这种情况下就只能回退到原始的13tap采样策略了:
- http://docs.scipy.org/doc/scipy-0.16.0/reference/generated/scipy.optimize.minimize.html
- https://twitter.com/adamjmiles/status/683041184915263489
TAA的输出会做为bloom的输入,因此TAA要放到bloom之前。
色差效果是通过对同一张贴图的三个通道分别应用不同的radial offset(径向偏移)来实现,也就是需要做三次采样。
可以根据需要调整色差实现公式,不过为了保证明度Luminance不变,就需要让RGB三个通道的加权值之和等于1,也就是白色的颜色经过计算后还依然是白色。
https://www.shadertoy.com/view/XssGz8给了一个基于jittering而非RGB-lookup的实现方案。
ColorGrading用于调整颜色的范围,使得该亮的地方亮,该暗的地方暗,这里采用的方案为:
https://www.shadertoy.com/view/Ml3GWs
与其他各个方案的效果对比为:
https://www.shadertoy.com/view/MtjGWm
smooth-min/smooth-max的实现方案:
https://www.shadertoy.com/view/ltf3W2
实现的理论依据可以参考:
http://iquilezles.org/www/articles/smin/smin.htm
针对雾效,发现全局的线性雾还是不能满足需要,还需要做精细的调教,主要有如下几方面的应用场景需要关注:
- Underwater
- Flash lights
- Dusty air
手电筒的体积雾效果
体积雾是通过ray marching实现的,这里给出基本原理介绍。
最标准的方法,屏幕空间的每个像素需要做128次采样,效果可以,但是性能成问题
如果将采样数从128降低到24,耗时从22ms到3.6ms,但质量同时也有较大的下降
同样的采样数,但是针对不同的像素,采用不同的起始采样距离,做一个采样点的抖动,效果略有好转,但是噪声比较明显(为啥从3.2到4.9了,更改了一个起始位置有这么大的差距吗?)
如果将采样数继续降低到每条射线3个,并使用白噪声作为随机数,基本上就不能看了
尝试将随机数更改为Bayer8x8,发现噪声要少一些,不过产生了明显的pattern。
之前还尝试过Interleaved Gradient Noise,后面发现,虽然这种方案计算非常快,但是产生的莫尔条纹,无法通过滤波的方式消除,就跟Bayer的一样。
一个好的随机信号应该满足如下几个条件:
- 所有的数值都包含在很小的一块区域里(所以小贴图就够用了?)
- 人眼对结构化的信号比较敏感,因此随机信号应该避免结构化,在小区域内的数值变化相对快速(相当于一个高通滤波)
- 局部区域应该具有尽可能多的多样性(?)
综合来看,我们需要的是一个高频噪声信号,而Bayer矩阵除了数据是结构化的之外,其他条件都非常符合。
按照上面的标准,蓝噪声很好的满足了条件:
- 噪声频率高
- 局部区域中数值比较丰富(即任何像素的相邻像素的数值不一样性比较强)
然而实测发现,在采样数目较少的区域会出现较为明显的瑕疵(高频变化区域),而在低频变化区域的表现则相对好一些。
均匀(uniform)噪声没有pattern,但是会有明显的噪声。
对比上面几种信号:
- Bayer信号最为uniform(结合上下文,这里说的应该不是uniform noise的意思),但是pattern明显
- Interleaved Gradient Noise比Bayer快5%,但是使用Bayer可以用较少的采样数而不会有明显瑕疵
- 蓝色噪声的采样效果不错,但在采样不足的区域会从瑕疵退回成噪声形式,我们可以用较少的采样数来实现类似效果
这里介绍了ray marching采样点的一点优化技巧:
- 采样点并不会从相机到射线的命中点全路径进行采样,而是聚焦在light frustum跟fog volume相交的部分
- 需要获取到light frustum的front depth跟backface depth(相对于相机而言)
这里描述了如何找到light frustum跟box相交后的新的plane set的:
- 这个过程发生在光源frustum与fog volume其一发生变化的时候,不会每帧计算
- 整个算法的实现逻辑跟3D Sutherland-Hodgman算法其实是一致的,整个实现过程用的都是静态的面片列表,而平面的相交计算,最后可以等价转化为1D的InverseLerp逻辑,计算非常高效
裁剪完成后,开始对裁剪后的frustum进行绘制,分为两个pass:
- 第一个pass,绘制front face,将depth写入
- 第二个pass,绘制back face,将上一个pass的depth作为input texture,并基于两个depth的范围做ray marching
展示了效果,这里light frustum分为上下两部分,上半部分为水面之上,采用默认的光照衰减逻辑,下半部分为水面之下,用于实现动画的焦散效果,不做衰减。
这里通过box来定义volume light的形状,方便美术同学控制来实现各种效果,这里提供了一系列的参数供美术同学调整:
- volume身上的参数,如密度、颜色等
- 光照的参数,如cookie,衰减、阴影等
有一些效果是低频的、光滑的,没有必要做全分辨率的采样计算。
比如我们可以将ray marching逻辑放到半分辨率下来实现,即前面两个pass都在1/2分辨率上进行,最后叠加一个depth aware的上采样+blur来得到较好的效果。
这里参考DICE的SSR实现策略,http://www.frostbite.com/2015/08/stochastic-screen-space-reflections/,在蓝噪声的基础上叠加了4次噪声模糊采样来消除半分辨率上采样导致的结构性pattern。
之后叠加TAA来实现效果的平滑。
最后在1080p上,只需要花费0.75ms即可得到不错的表现(半分辨率下只做6次采样,也就是全分辨率模式下,每个像素只采样1.5次)。
这里补充介绍了一些实现细节:
- 因为是半分辨率采样,因此shadowmap跟depth map的精度不需要太高,可以考虑采用16bit的,同时cookie贴图跟shadowmap的分辨率也可以相应降低
- 总的来说0.75ms的消耗其中有0.3是花在6个sample的采样上,0.4花在depth aware的upscaling上
- 大部分光源并不需要全屏计算,也就是说大部分光源在采样的时候每个像素只需要采样三次(半分辨率下)
这里之所以纠结于采样数,是因为当前采用的技术路径会因为众多的贴图采样而存在性能瓶颈,这里介绍了NVidia的Deinterleaved Texture方案,该方案多用于需要按照一定的采样分布对同一张贴图做多次采样的情况,由于对于同一个像素而言,其采样的多个贴图纹素分布较为离散,容易导致cache miss,为了消除这种问题,尝试将一张贴图按照采样点的分布分割成多张,比如四张,之后每个像素分别采样四张贴图的某个位置,相邻像素采样的位置是相邻的,从而可以做到cache efficient,具体参见:Deinterleaved Texturing for Cache-Efficient Interleaved Sampling
- 这个技术多用于SSAO、SSDO、SSR等需要对某张贴图做大量的、离散的、带有一定抖动模式的应用情景(相邻像素的采样pattern,即采样点的偏移是各不相同的,如果是固定offset的,在空间局部性上是可以有较好保证的)
- 实测性能可以提升2.3倍(有这么厉害?)
在这块还有一些遗留的工作:
- 对cookie做重要性采样,比如对spotlight做等角度采样
- 在采样点分布随时间的变化逻辑中,增加蓝噪声的控制以提升变化度
阴影贴图的生命周期覆盖lights pass跟volume lighting pass;而volume light部分绘制的半分辨率buffer的生命周期会持续到半透pass,与后续半透部分的做排序(?)
在上面的体积光效果中,利用了射线的随机初始步长,同时还借助噪声的随机性来降低了采样数目。
除了这个应用案例之外,还有很多其他的随机采样的case,比如这里用于DOF,同样借助TAA来做滤波,相关细节有:
- DOF在不同的位置使用不同的模糊半径,模糊半径大的时候,通常需要采样较多的样本
- 样本数较多会导致性能的下降,这里将样本数限制在4个
- 采样的时候做了Depth aware的处理(样本挑选),不知道有没有在半分辨率下进行计算,但肯定借助了TAA的时间复用能力降低了噪声
软影效果同样需要对相邻像素做多次采样,同样可以借助TAA来完成滤波降噪。
按照这种策略,每个像素采样四个sample,在部分平台下的性能表现甚至优于单tap的PCF
样本分布采用了圆周方式:
- 每个样本的半径跟其他样本都是不同的,假设分成四个圈,每个样本都落在一个圈上面
- 样本跟圆形的连线,与相邻样本连线的夹角是相同的,只不过每一帧所有连线都会朝着一个方向转动一个相同的角度
接下来看看怎么通过Dithering方法消除颜色的banding问题,参考的是Banding in Games:A Noisy Rant的解决方案
该方案在【Learning Notes】Banding in Games - A Noisy Rant中有介绍
这个图中有较多不仔细看不太会注意到的噪声(使用三角形PDF的噪声做了Dithering)
这是修复后的结果
修复前的问题表现如上图所示
banding问题会对质量造成很严重的影响
之所以会有banding问题,主要还是因为精度不足导致
解决的方案就是叠加Dither的noise
Dither就是在原来输出color的逻辑中,添加一个随机数。
这里对floor、ceil等trunc、quantize逻辑的表现进行分析
叠加随机数之后,误差跟方差表现就变成上面这样
https://www.shadertoy.com/view/ltBSRG,banding问题就没了
噪声调制方法,可以有效减轻图片质量比较差的问题,不过这里的效果仔细看还是会有一些瑕疵,人眼比较倾向于噪声比较均匀的情况。
三角PDF噪声(TPDF)的误差跟原始信号是独立的,且是具有此特性的最简单的噪声信号,相比起来高斯信号就复杂多了。
TPDF的生成有两种方案:
- 添加到均匀分布上 (比如 (rand(seed0)+rand(seed1))/2)
- Reshape一个均匀分布 (参考e.g https://www.shadertoy.com/view/4t2SDh )
用了这个信号之后,banding问题效果好多了,不过最大误差现在是2LSB(least significant bit)
虽然banding不见了,但是噪声太强了
采用蓝噪声,效果会好很多,参考:https://www.shadertoy.com/view/ltBSRG
视觉感知上,噪声感就降低了很多
最后用的噪声,会在ALU-based RPDF以及TPDF之间变化,并且会使用离线生成的蓝噪声来进行调制:
- 会将四个种子烘焙到256x256的RGBA贴图,每个通道8bit
- 需要的时候,只需要对这个贴图进行采样即可得到噪声,如上图公式所示
拿到蓝噪声之后,只需要使用一个remap就能将之转换成TPDF噪声,如上图公式所示
其他更多信息参考:
optimised remapping by forsyth
https://www.shadertoy.com/view/4t2SDh
blue noise by Timothy Lottes
https://www.shadertoy.com/view/4sBSDW
blue noise array using generational algorithms
http://excedrin.media.mit.edu/wp-content/uploads/sites/10/2013/07/spie97newbern.pdf
这个banding效果发生在光照部分,所以dither应该发生在lighting pass中。
不过用完发现,banding问题有所好转,但还依然能够辨认,这是因为最后pass写入到了低精度的buffer中。
如果在finalpass也应用dithering逻辑,banding就彻底不存在了。
透明物件如雾效例子也会有类似问题,如果按照上述逻辑,每个像素都要走一遍dithering逻辑,同一个位置的像素就会需要叠加多次,这个在multiplicative混合模式下(additive跟subtractive就没事)会导致额外的效果问题。
但是我们在渲染的时候很难知道每个像素应该采样的噪声强度是多少,这里的解决方案是暴露一个噪声amount的参数给美术同学,在离线的时候设置好(能解决所有问题吗?)。
最终给每个pass都加上dithering逻辑,会手动将中间的rt转换到sRGB空间来提高质量。
从性能角度考虑,可以将dithering放到低分辨率pass中,虽然之前担心产生的噪声pixel尺寸大于一个像素会很成问题,但实际测试看起来也还好。
此外,如果转换到sRGB的代价太高的话,也可以就在线性空间添加噪声。
这个方法还可以用于消除法线精度不足导致的高光banding问题。
最后的两点注意事项:
- 为噪声添加动画,质量会更好,可以借助TAA来掩盖噪声
- 叠加噪声之后要注意颜色的范围,避免硬件颜色空间转换会导致新的问题
接下来看看光照相关的内容。
先看看自定义灯光,Inside用的是延迟渲染(light prepass是啥意思?),因此可以支持各种类型的光源,先来看看上图中的三种自定义光照。
Bounced Light是一个点光,用于模拟GI效果的,采用的光照shading逻辑不是单纯的点乘,而是做了一些额外处理,如上图所示,从而可以消除光的方向性,使得结果更平滑
在需要的地方,还可以完全过滤掉法线的影响,使得结果更接近环境光的表现。
这个光不只是用于静态场景,也可以用于动态情景,比如手电筒、动态升降的窗户等。
不像普通的点光,需要摆放多盏来覆盖一个大块的区域,比如一条走廊或一个大的室内,这里直接用一个非均匀的形状来覆盖大块区域,比如一个压扁的药片形状、一个拉伸的按钮等,从而可以更好的减少overdraw。
shading逻辑由于可以较好的自定义,因此可以不局限于光源的additive模式,还可以考虑multiplicative模式,从而可以用来实现AO效果。
参考 Android Lesson Five: An Introduction to Blending:
glBlendFunc(source factor, destination factor)
{
output = (source factor * source fragment) + (destination factor * destination fragment)
}
// ----
Additive blending --> glBlendFunc(GL_ONE, GL_ONE) -->
output = source fragment + destination fragment
// ----
Multiplicativeblending --> glBlendFunc(GL_DST_COLOR, GL_ZERO) -->
output = source fragment * destination fragment
在选型的时候,之所以没有使用SSAO,是因为SSAO在局部细节控制上不太灵活,且由于屏幕空间的原因,会有各种瑕疵。
AO Decal有三种类型,如上图所示,给了效果应用前后的对比。
这里展示了两种不同叠加模式下的效果表现。
先来看看Point类型,Point指的是点光,不过这里不是用Additive模式,而是用的Multiplicative模式,右边给出了两种模式的效果展示。
Point类型的AO Decal主要用于角色,每根骨骼上挂一个,这样就能在地表上、物件上以及两根骨骼交界的地方能够产生出阴影的效果。
上面有16个角色,每个角色16根骨骼,因此总共是256个point decal。
这里给出具体的计算公式。
接下来是一个球形遮挡体对物体表面投射的AO效果,因为想要的是中间的硬边遮挡(为啥)而非左边的软边遮挡,因此公式上做了一下调整。
思路来自于:http://www.iquilezles.org/www/articles/sphereao/sphereao.htm
受https://www.shadertoy.com/view/4djXDy启发,给出了box形状的decal效果,当然,是经验式,而非物理正确。
Shadow Decal用于给不投射阴影的光源以及环境光添加阴影效果
这个是添加了之后的效果。
通过一个box decal来实现,带有alpha通道的贴图,可以用于实现软影效果。
decal的作用是与light buffer叠加。
如上图所示,每个物件都要挂一个贴花。。。
可以支持动态效果,如上图中角色跑动、推动箱子时阴影的变化,以及下图闸门的开关导致的阴影变化。
因为shadow decal数量太多,因此希望降低其整体计算成本
这里通过调整思路,只需要一个MAD指令就能在PS中得到世界空间坐标,不过后面还有一个矩阵运算。
不过如果把上述公式改成在VS中算出模型空间的射线,那最后在PS中就只需要一个MAD就能得到结果。
另外一种要介绍的是SSR Decal,不是通过后处理得到,而是采用一种局部的方法实现,但是其基本的实现思路是一样的,还是需要走屏幕空间的ray tracing,不过需要tracing的范围是局部的而非全屏幕的容易。
在计算之前,需要先定义一系列的参数:
Backup color, which is what we fade to in case our ray misses.
Texture that represents the puddle shape.
Fresnel power to control opacity of the reflection.
And finally trace distance, which is the only trace-affecting parameter, it simply says how far the ray needs to go, from 0 which travels nowhere to 1 which traverses entire screen.
SSR的问题依然存在,不可见的部分依然是缺失的,这里做了一些处理:沿着屏幕的边缘fade out。
SSR的另一个问题是遮挡信息是不正确的,比如小男孩背后有块大石头,如果小男孩跳进湖里,就会出现ghost效果。
针对这个问题,这里也做了处理
先为小男孩画一个屏幕空间的boundingbox,当射线进入这个区域,就表示遮挡信息可能会出问题了。
不过如果射线没有命中小男孩,那这种情况就无需考虑
如果命中了
就把射线拉回到boundingbox上距离该点最近的水平exit点
并从该点继续tracing
这里介绍另一种不需要boundingbox的方法。
假设男孩在一座干草堆前面。
发射一条射线
标注出来了我们射线追踪不到的区域
把这些区域连起来可以看成是一个heightmap,我们trace得到的就是一个拉长版的男孩。
来看下代码
但是我们不想要一个拉长版的男孩,所以这里在trace结束的地方加了一点处理,当diff差异比较大的时候,我们认为是有问题的,那么就继续trace,直到命中到一个差异不大的点。
不过实际情况不会这么平滑,这里采样是离散的,因此更为准确的示意图如上所示
这里画出两条蓝线,我们需要拿到射线与这两条蓝线的交点来做结果的缝合。
这里需要添加一些状态
先记录下未相交的点
接着到第一个相交的点,
之后继续trace到第二个相交的点
当我们发现两者之间差值大于某个阈值,就启动blend计算,将两个相交点位置的结果做一个混合。
虽然技巧很hack,但是效果还不错。
屏幕空间trace需要将trace方向投射到frustum空间,采用常规的矩阵运算消耗太高
可以考虑在CPU完成DirProject的计算,之后在PS中就不用做矩阵运算了,不过还是需要做一些除法。
不过除法可以通过这种方式规避
经过上述运算之后,我们就得到了上图所示的阶跃效果的tracing结果,为了优化表现,仿造volume fog,添加一些dither跟jitter处理,得到右图结果。
白噪声虽然可以用,但是其local neighborhood histogram(局部直方图)是变化的。
bayer matrix虽然没有上述的问题,但是基于这个信号得到的效果会有明显的pattern感。
而蓝噪声则表现最好,两种问题都不存在。
之所以希望相邻区域的直方图分布接近相似,是因为最后希望借助TAA的时域滤波的能力来实现相邻数据的复用,而TAA的实现策略中会基于相似性拒绝掉一些跑飞的点,而如果相邻数据的直方图差异大,那么拒绝的概率就高,复用的采样数目就少,就会导致效果的不平滑。
在进行光线trace的过程中,需要找到射线与depth的交点,不过由于这里不知道当前点采样位置的物件的厚度(墙壁的厚度),就有可能导致下一个采样点已经跳过了墙体,从而取得错误的结果。
这里采取的办法是计算当前采样点的z值(从下文来看,这里说的是该点沿着视线反方向投影到depth map上的点)跟该点对应的depth数据的差值(假设以depth作为height field的话,这个值就可以看成是垂直高度,带符号),当差值小于某个阈值(墙体厚度),即可以认为trace到合适的点。
这里有一些地方没有解释清楚,比如:
- 为啥一定要大于0才终止,小于0(trace到墙体)为啥不终止,
- 还是没有解决trace step高于墙体厚度的问题
另外,这里提到上述方法在视线方向与墙体(这里将墙体作为反射平面看了,如果是这样,为啥需要关注墙体厚度呢,想不明白)呈现45度夹角(反射射线跟入射射线正好垂直)时,这种情况下会导致Delta持续为0
后面看能不能通过视频做一下补充,先把原文列在这里:
Our first implementation as a simple constant and a slider for it, but it was hard to use and the results were unpleasant.
Second round involved using the delta of the screen reflection ray’s Z, so if we’ve come into a wall between last step and this, we’re happy.
But this had problems when looking at walls that are 45 degrees to the viewer generating reflection rays that are perpendicular to the view direction. In that case, we have a 0 Z delta, and only move in X and Y.
为了解决这个问题,这里决定把thickness的计算公式捋一下(为啥跟thickness杠上了?)
abs展开之后得到的公式如上图所示(一直不理解thickness为啥是abs,又是为啥可以展开成这个公式的,其中ref1代表的时啥意思,dirProject又是啥。。。)
经过上述展开后发现,问题就出在反射方向上。
如果把反射项refl.z去掉,结果就好多了。
另一个具有反射需求的是水体,不过水体没有用SSR,因为边界缺失过于严重,效果不好。
这里将水看成是一层层的特性叠加得到,包括fog、折射、反射等,渲染的时候也遵循这个顺序。
首先是fog,这是基于水体表面到背后物件的距离计算得到。
接着是水下的透明物件
接着是折射,这个pass会以前面一个pass的color为输入,之后基于如下逻辑得到结果:
- 从蓝噪声中读取一个随机值
- 基于随机值对surface的法线做扰动,基于扰动后的法线计算出uv的offset,并得到这个offset后的uv(这里不需要ray trace,只需要通过一个CS对每个屏幕空间像素,基于该像素的世界空间坐标与水体平面的交点,获取到该点的法线,之后基于这个法线加随机值对前面输入的uv做扰动即可)
- 基于上述uv对输入贴图进行采样,得到扰动结果
之后是反射结果,并基于菲涅尔项控制反射与折射的混合比例。
上面是相机处于水上的渲染顺序,而如果相机处于水下,渲染的逻辑会需要做下调整。
首先是水面上方的反射效果。这里没有采用菲涅尔项,而是直接使用了一个smoothstep函数,输入的两个参数数值比较接近,通过这种方法快速使得随着距离的推进快速进入全反射,并将结果置为黑色。
接着是水体的雾效,从相机出发到物件或者水体表面之间最近的点。
然后是折射效果,这里之所以要将雾效放前面,是因为希望雾效能与DOF有一个较好的结合(是说DOF放到了折射部分逻辑里吗?)
最后才是水下的透明物体,之所以放最后,是因为不希望这些物件被折射的DOF blur或者扭曲(水上的透明物体则是跟之前一样,在此前就完成绘制,后面会跟不透物件一起参与DOF跟扭曲)。
前面说的是对于水体的某个部分,需要经历多层的效果叠加。
而实际上,水体需要绘制三个部分。
首先是水体与距离镜头某个距离(目前设定是6~12m)的一个平面相交的起伏线(displacement edge)。
之所以需要有这一步,是为了保证相机在往前进入水面的时候,不至于感觉到水体是薄薄的一张纸。
而前面的距离是经过实际测试给出的经验值,因为距离超出某个阈值后,就不太能关注到视差效果了(所以需要保持在某个范围里)。
水面mesh是一个屏幕空间的mesh,mesh的顶点分布在屏幕空间中是线性的(类似于projected grid)。
接下来是水体volume的外边界(outter sides),用于模拟displacement edge无法模拟的效果。
这里不会做多遍绘制,会通过stencil取用第一遍绘制的数据(按照从前到后的顺序进行)。不过这里还是不太懂,outter sides绘制的mesh是长啥样的。
以box为例,所谓的外边界,指的就是其front faces(displacement edges不也是front face吗?)。
这里的疑问是,为啥需要设置一个外边界,而不是直接用displacement edge完全覆盖呢?
最后是水下部分,这里也会启用stencil reject,从而避免覆盖到外边界的区域。
同样以box为例,这部分覆盖的就是back face部分。
接下来介绍一下特效表现,先来看下smoke效果
没有添加smoke
添加了smoke,即使游戏暂停的时候,smoke也会动态变化,另外可以注意到smoke会接受来自地面的渐变光照。
如果把这部分灯光禁用的话,由于环境光效果跟房间颜色比较接近,就会是的smoke看不太出来,接下来我们逐个把效果加回去。
首先是来自地表的光照(?),主要用于模拟GI效果,这里粒子部分的光照是通过点光来模拟的。
接着添加一个垂直方向上的渐变,这是为了模拟距离光照比较远的地方的AO效果。
这是效果
最后添加一些distortion来制造流动效果。
distortion是通过swirl噪声贴图来实现的
具体而言,就是将该噪声信号按照一定的规律投影到世界空间上,这个投影会带有随机的offset,之后使之沿着某个给定的方向滚动,这个视频中给出的是向下的方向,这是smoke流动的方向。
接下来看看火焰效果。
火焰由多个效果组成:火焰本身、烟雾、火星子以及光照。
火焰的效果跟前面的烟雾的实现逻辑一样,同样的扭曲,不过这次方向是向上的(火焰向上。。)
要想实现正确的火焰效果,最重要的是颜色。
这里的做法是一个类似于延迟管线的逻辑,将火焰渲染到一张alpha buffer也就是bloom buffer中,只保留alpha(强度、温度?)
之后通过一张LUT,实现从alpha到color的映射,从而保障颜色的正确
通过这种方法,不但可以很灵活的控制颜色效果,还可以避免多层混合导致的效果异常。
接着,只有扭曲效果就不太够了,需要更丰富的效果,这里用了序列帧的概念。
常用的方法有两种:
- 在序列中循环往复,导致的问题是重复感太强
- 在序列中随机选择,导致的问题是有可能前后两帧选择了同一个图案,于是导致像是卡了一下的问题
这里的方法是结合起来,在水平方向循环,在垂直方向随机(为啥不做不放回随机抽样呢?)
在相邻两个序列帧之间,还做了基于时间权重的混合,避免了硬切的问题
而直接渐变的话,看起来会有点假,由于火焰看着像是在往上升,因此还做了从下往上的gradient,看起来就像是新的序列帧从下往上逐渐取代老的序列帧
当然,为了避免切换过于干净不够真实,这里还加了一些随机噪声,使得切换效果更为自然。
接下来介绍一下不用后处理方式实现的Lens Flares
这里的做法,就是在对应的光源(如手电筒)上绑定一个sprite,之后就基于光源的位置绘制该sprite(有一定的空间尺寸,面向相机)
怎么做到光照强度随着光源被遮挡的范围平滑变化呢?
这里的做法是在VS中按照一个pattern(黄金分割螺旋结构)对顶点周边的位置的深度贴图进行采样(这里说采样32次),并与该位置的深度进行判断用于估算该顶点周边区域的可见性(有点PCF的意思),这个可见性会在PS中用于调控输出颜色的强弱。
当然,深度贴图的采样点不会落在最边缘的位置,而是偏中心的位置(前面说的采样32次是啥意思?)
且不会是一个点,而是四个点,位于中心点跟边缘点连线的某个位置(中心点往前移动10%),这样就能很自然的实现光照的gradient(平滑)效果了
接下来看看水体效果
首先是小男孩跳入水中时,在水底行程的浮沫效果。
整体渲染思路跟前面烟雾的逻辑基本一致,这里就不深入细节了。
这里是移除了distortion后的效果,可以看到,缺失了此前bubble的移动效果细节
把光照也移除之后,会发现效果更为扁平,缺失了此前浮沫的深度信息表现。
这里给了个视频
接下来看看探照灯下的水体表现,主要分为三层效果。
首先,是雨效。
这里尝试了三种方法:
- 用后处理,采样贴图,并做uv动画滚动向下。这种方案缺少了视差信息,不真实
- 采用粒子特效。CPU侧生成粒子耗时太高。
- 采用公告板粒子,但是发现overdraw太高了
最后用了顶点动画方案。
上图展示了线框模式下的雨效面片。
这里通过统一的调度逻辑来实现这些面片的摆放,避免overdraw,同时由于不需要CPU生成数据,所以这里的消耗也没有了。
总体来说分为两种效果:
- 雨丝
- 水花
针对两种效果,分别做了一些细节处理,这里不展开:
One type is the falling lines, the animation here is just move down from the top to the bottom of the mesh’s bounding box.
Then there are the splashes, they all scale up from 0 to full in their animation and fade out from some intensity to 0.
第二个效果是体积光,这个在前面已经介绍过了,这里就不多说
最后是漫反射跟高光反射
合并到一起的效果
水下部分
有跟雨效类似的水底尘埃效果,采用的也是顶点动画方案,跟前面一样。
水下的体积光,会对贴图做一些焦散动画效果
最后的高光,不过这里不是反射而是折射。
再来看看小男孩在水中游泳时的表现。
涟漪效果是通过mesh particle(环形的mesh,不断往外扩散出去)实现,shading逻辑比较简单,主要是基于法线的折射跟反射。
而法线则是基于当前点到中心点的距离计算得到的。
上述数值会乘上一个sin(sin going from 0 to 2pi/tau, to get 0 at the edges and the middle),1/3指向内部,2/3指向外部。
接下来看看水体的漩涡效果。
这是演示效果
这里的水体不是一个cube而是一个圆柱,虽然水体的流向效果可以通过缩放跟fade实现,不过这里用的是另一种方法
UV动画,不过这里的UV映射是径向的,且由于水体mesh顶点密度高,所以计算逻辑可以放到顶点shader中。
贴图包含基色跟法线,基色主要是浮沫效果,需要找到一种支持tiling,且在scrolling的时候不会出现太明显下次的贴图数据。
最后用的是这种名字叫European fan(欧洲扇形)或wave cobblestone(波形鹅卵石)的贴图,这种贴图本身也是tiling的,不过不是像传统贴图一样沿着UV方向tiling
而是按照鱼鳞的方式。
之后将这张贴图按照前面说的径向映射的方式贴到mesh上,并做UV滚动
就得到最终效果。
最后看下最终的洪水效果。
最终的效果其实是前面介绍的多种技术的结合下实现的
首先是基于此前的浮沫贴图的一个水体的volume,这个volume是可以形变的,包含三个morph target(或者说blend shape),分别对应前中后三个阶段。
这里只在水体部分做了Tessellation,且将浮沫贴图沿着水体流动的方向进行滚动。
接着在水体冲出的时候,在地表添加一张贴花,这个贴花会启用SSR
为了提高真实感,这里贴图的uv动画效果在冲出区域是快速的,而随着往前推移则逐步减速(UV滚动的速度在前端是按照pow的非线性的方式推进的,后面逐步减速,最后变成线性的)。
最后是浮沫效果,采用同样的贴图uv滚动速度,从背后进行照明
最后是将贴花跟洪水融合到一起的一个特效
最后添加一些喷射的效果
总结:
- 尽量多用蓝噪声
- TAA跟噪声搭配非常好
- 尽量采用Dither来掩盖瑕疵
- 其他