【Learning Notes】Banding in Games - A Noisy Rant
今天给大家分享的是一种通过噪声+Dither来消除banding异常的方法,该方法的作者是Playdead工作室(开发过广受好评的游戏《地狱边境》与《Inside》)的Mikkel Gjøl,原文可以在参考链接中找到。
这是pixeljunk eden的一个截图
通过调整整张图的明暗对比度,我们会发现图片会逐渐呈现色块化。
上古卷轴,大家印象深刻的是场景里的内容
不过这里关注的是菜单界面的banding问题
同样做下对比度的缩放
kentucky route zero是另一款非常有趣的游戏
也存在同样的问题
这里粗略的展示了一帧的渲染管线,对显示器而言,其希望得到的输入贴图是sRGB(standard RGB,提供一种标准方法来定义色彩,让显示、打印和扫描等计算机外部设备与应用软件对于色彩有一个共通的语言,非线性空间)的,而为了保障输入贴图的精度,就得做到:
- 要么单个通道的位数高于8(从而提供更高的精度,保留精度冗余)
- 要么上述所有环节输出的颜色都是处于sRGB空间的
而实际上我们不太可能提高贴图输出的位数,另外,要保证每个环节的输出结果符合标准也比较困难,会有各种原因导致这个的失败,比如我们的framebuffer通常默认是线性空间的(计算逻辑通常都是线性空间),如果想将之转换到sRGB空间(比如通过OpenGL的framebuffer_sRGB),就会有计算上的额外消耗,此外从sRGB的framebuffer读取结果并与当前color混合后的结果不一定还处于sRGB空间。
如果gamma设置不正确就容易出现前面的banding问题,但是考虑到各种因素的存在,想要把gamma设定正确也不容易(RTT是render to texture的缩写)。
先来回顾一下sRGB的相关概念
建议先看看这篇文章:Gamefest 2007: Picture Perfect: Gamma Through the Rendering Pipeline,这篇文章给出了gamma的前因后果描述,并给出了实践过程中的一些指导建议。
- 基色贴图由于需要直接查看效果,在显示器的gamma矫正下,为了保证结果的准确,需要在编辑与保存的时候采用sRGB
- 还有利于存储暗部细节(人眼对光的感知是非线性的,感受的是相对亮度,而非绝对亮度,相当于开了一个系数为1/2.2的gamma矫正)
- 其他类型贴图,如果单通道位数高于8,有精度冗余,可以考虑用sRGB(后续计算需要转回线性,此处会有精度损失),否则用线性
- shader计算应该在线性空间中完成,否则计算结果会不正确
- shader计算完成准备写入到framebuffer的时候,需要转换回sRGB,framebuffer也应该使用sRGB格式
- 这里会导致blending存在一些问题,为了避免这类问题,建议采用HDR的framebuffer
- 可以采用线性空间进行数据存储,不用担心精度问题,且混合结果符合预期
- 有足够的精度存储暗部
- 这里会导致blending存在一些问题,为了避免这类问题,建议采用HDR的framebuffer
- 更多HDR的信息参考下图
消除banding的一个手段是添加噪声
容易出现banding问题的情况(不考虑贴图压缩导致的banding)都是那些会有gradient的情景:
- 光照
- 雾效
- 半透混合
- 粒子效果(半透?)
- 后处理比如Glow
- AA
这里给了集中通过Dither(添加噪声)来消除banding问题的效果图,具体实现参考Shadertoy代码
也可以尝试一些其他的稀奇古怪的噪声pattern
这里介绍了一种效果较好的Dither方法,不过这些方法是基于error-diffusion的,需要对相邻区域做大量采样,跟GPU的特点不太吻合:
Unity的Lightbuffer,为了支持HDR Lighting,已经切换到对数空间(而非线性空间),因此将之直接转换到sRGB就不可行了(HDR Lighting为啥需要对数空间?)
这里的解决方案是在内置的Prepass Lighting Pass中添加一些随机的dither处理(这里的做法是将噪声从原始结果中减去,而非加上,所以导致结果会有点暗,但是为啥要减去呢?)
虽然有些噪点,但是拉远了来看就不会注意到了,而且相对于此前的banding效果,这个效果更容易接受。
正常的truncation采用的是floor函数,所以颜色通常相对于原始信号会更低一些。
最大误差是1 LSB(Least-Signicant-Bit),平均误差是0.5LSB
如果把信号+0.5LSB,那么整体的亮度就跟原始信号持平了。
这个时候信号相对于原始信号就是有增有减的,最大误差是0.5LSB,整体结果是无偏的(no bias)
前面是加一个固定数,所以banding问题还存在,如果我们将叠加的数值变成一个平均值为0.5LSB的随机数,比如这里采用的白噪声,
平均误差累加之后依然是0,但是最大误差是1LSB
均匀分布的噪声,单个数值的累计误差是0。
三角噪声,如上图公式所称,是一种更好的PDF,通常用于音频的抖动,这种PDF可以在低频区域添加噪声,使得噪声更为均匀,不过wiki上说这种噪声还需要做进一步研究,所以作者最终也没有使用这种噪声(实际上,为了有更好的效果,在GPU的round算法下,噪声的抖动范围应该是-1到1,而非-0.5到1.5,所以如果使用上述噪声的话,在使用上公式应该调整成上图红色文字部分)。
这里给了不同噪声的误差表现,具体可以参考 noise distributions,从左到右依次是:
- uniform noise
- triangular noise
- gaussianish noise
- moar gaussianish noise
将hash公式从两个改成一个,效率会更高一些,此外将一个hash remap到triangle PDF效率也会更高,作者还给出了一个更高效的版本,具体可以参考:A faster triangle noise
这里对比了多种不同的概率分布信号,其中高斯表现最好,但是性能稍差,最终采用了三角分布。
这里展示了abs(error)截图,不过就看不到正负误差相互取消的效果了。
用了两种不同分布的噪声来消除banding问题,三角噪声在banding消除上更好(不是太看得出来),不过噪声也更大一点。
此前介绍的是噪声叠加,这里展示了删减噪声的效果,其实是一样的,不过是额外加了一个偏移。
另外这里需要注意,噪声的叠加需要在trunc(也就是计算完成输出到framebuffer之前)之前进行,否则banding问题会依然存在。
这个方法能生效的前提是中间计算数据的精度是足够的,不会出现中间精度不足就已经banding的问题。
正常计算的时候,颜色数据是在线性空间的。
dither就是在颜色输出之前叠加一个噪声信号。
不过如果输出的颜色需要转换到sRGB空间,那就先转换,再叠加。
对数空间数据也是同样的
这就不太明白了,为啥不直接在线性空间做dither,还需要先转换到sRGB,dither之后,再转回线性空间,是因为直接在线性空间dither会导致数值超出范围吗?
(sqrt(c)+n)^2 == c + 2nsqrt(c) + n^2,不过pow( sqrt( c ) + rnd/255.0, 2.0 )可能会更快,具体参考LinearDitherForSRGB
这里解释了为啥需要额外做一遍线性到SRGB,再重sRGB到线性的转换,从效果上来看,前者质量会更高一点。
steam的菜单图片,将噪声应用到错误的空间后,banding问题还依然存在。
这里说到了,由于clamp的存在,接近两个端点(黑白)的区域,如果添加2LSB的dithering,结果就还是会有问题
解决方案是在边界区域,退化成1LSB的均匀分布
这里给出计算公式
叠加方式有多种,甚至还可以自己发明新的合适的,其中:
- additive跟subtractive模式不会改变信号的幅度,因此直接使用噪声算法是没问题的
- 不过由于半透会有多次叠加,因此假设单次有1LSB的增幅,那多次之后结果就可能变形了
如果某个像素有多层半透覆盖,那么每一层使用不同的噪声的话,效果就好很多了
粒子特效等物件就符合这种情况。
针对multiply模式,还会有其他问题,这里给出了一些解决方案
dither模式在multiply模式下作用就不明显了,上图左侧展示了noise太小跟加大之后的效果对比
更多关于alpha blending的实现细节:
additive blending with SRC_ALPHA means adding noise before multiplying! premult: then at least the incoming signal is ok… dithering alpha depends on how it’s used in the blend-function… prefer pre-multiplied alpha to SRCALPHA, ONE_MINUS_SRCALPHA http://home.comcast.net/~tom_forsyth/blog.wiki.html#[[Premultiplied%20alpha] ] …allows to blend colors smaller than 1/255 (as they are dithered afterwards)
数值调大之后的效果对比,17是经验值,完全由美术同学掌控。
这里推测精度不足有可能导致该方案失效。
另一个应对思路是将blending放到shader中完成,但是这个性能实在是太差了,相比起来sRGB以及高精度的buffer可能更靠谱一点。
后处理也会有多种效果会面临banding问题,通常为了性能考虑(带宽),贴图精度大多是8位的,也就容易导致问题。
后处理如果需要做多次贴图的输出,那么噪声的叠加就应该在每一步都应用上
这里展示了数据空间以及叠加噪声前后的对比,可以看到sRGB+Dither的方法效果最好(实测发现,只加噪声,不开sRGB也够用了)。
limbo是全灰度的,不同平台上RT的格式有所不同:
- PC/XBox:10:10:10:2
- PS3:RGBA16
会在tonemapping的时候叠加噪声。
Color-Shift Dithering,没有详细解释这个概念的含义,根据字面意思以及下文的理解,是说为不同的颜色添加不同的dithering offset
//note: offset r, g, b, limbo-style
outcol = vec3(its, its + 1.0/3.0/256.0, its + 2.0/3.0/256.0);
为不同gray scale的颜色,添加不同的offset?
没太看懂这里的说明:
执行速度很快(?),不过只适用于grayscale(为啥?)
limbo用的偏蓝的offset,叠加3个 steps(?),如果改成偏红偏绿,基于颜色的luminance来做偏移,同样也是3个steps
dithering: Color Banding Removal 中给出了多种消除banding问题的方案,并给出了测试效果,包括各种dithering pattern以及一个color offset算法
最终给个结论:
- 尽可能的提高RT精度
- 尽可能的启用sRGB
- 采用三角噪声来消除噪声
- 噪声记得添加到合适的颜色空间
不过Dithering也并非完美的,也会引入一些副作用。
这个方案也可以用于消除GBuffer normal的相似问题
这里给了一个Audio Dithering的音频文件
作者给出的一些参考文档,下面是一些bonus slides。
http://sandervanrossen.blogspot.dk/2012/02/hdr-dithering.html
这里有个类似的问题,以为可以通过dithering的方法来消除,后面发现是光强调整有问题:
- a lightsource brighter than the sun, illuminating dark, black granite stone.
这里又有个类似的问题,后面定位发现是美术同学的模型设计如此。。。
参考
[1]. Banding in Games:A Noisy Rant