聊聊Unity的Gamma校正以及线性工作流
0x00 前言的前言
这篇小文其实是在清明节前后起的头,不过后来一度搁笔。一直到这周末才又想起来起的这个头还没有写完,所以还是直接用一个月前的开头,再将过程和结尾补齐。
0x01 前言
结束了在南方一周的出差,清明时节回到了刚好下过雪并且和南方有20多度温差的北京之后,终于有时间来写点文字了。这篇小文,我主要想来聊一聊在使用Unity时和gamma校正相关的话题。事实上关于Gamma校正的来源历史以及理论知识已经有很多相关的文章了,比如龚大的《gamma的传说》、Nvidia的Gpu Gems的文章等等。所以我在理论知识上只是稍作着墨,主要还是要来聊聊Unity中的Gamma校正的相关内容。
0x02 显示器和gamma校正
关于gamma校正来源的说法很多,具体可以参考龚大的《gamma的传说》的显示器说以及乐乐的《我理解的伽马校正》中所提到的人眼视觉特点说。两者都有道理,并且客观上这两个说法发生了有趣的巧合,最后达到了一个还不错的效果。
简单来说,过去的CRT显示器存在一个特点,即屏幕上显示的颜色对于传递而来的原始值并不是线性的(非线性) ,在这里非线性意味着以一个比率增加某个颜色分量,并不会导致显示器屏幕上的光强度增加相同的比例。举一个例子,假如我们将一个颜色的红色分量变成之前的两倍,显示器的屏幕所显示的红光并不会变成之前的两倍。
事实上CRT显示器的输入和输出之间的关系近似于一个指数关系,而这个指数便是我们常常听到的gamma。典型的gamma范围在2.0到2.4之间,一般该值常常以2.2作为折中。虽然后来的LCD并不存在这个特点,但是为了保证兼容,也选择了和当年CRT一样的非线性特性。
上图中的红色实心线是在gamma = 2.2的情况下,显示器实际显示色彩强度的方式。 这一部分是由显示器的特性导致的。所以如果图片不做任何处理,经过pow(2.2)的操作之后显然会变得更暗,所以gamma校正就显得很有必要了。而gamma校正要做的事情也十分简单,即通过pow(1/2.2)将颜色强度提高,也就是上方的红色虚线,这样经过显示器时就会将显示器的pow(2.2)抵消掉。
同时,人眼对暗部的变化更加敏感,而对亮部变化其实不是很敏感。这可以以摄像作为一个例子,使用摄像机时,摄像机会把进入到镜头内的光线亮度编码成图像中的像素。
例如人们看到下面这张图,会自然而然的认为中间的地方即灰度为0.5的地方。
事实上,摄像机“看到的”光线亮度如下图,上图中间部分的灰度其实只是在0.2左右。
所以在只有8bit的情况下,没有必要在亮部浪费过多,这样就可以表现更多暗部的细节变化,所以实际亮度只有0.2经过gamma校正后实际被编码成了0.5的像素值。
当然,我个人认为显示器的特性是gamma校正出现的主要原因,而人眼对暗部的敏感而出现的gamma校正则更像是为了适应显示器这种特性而为的一种编码策略的“优化”。
0x03 硬件实现
sRGB 颜色空间是一个可以直接用来在显示器上显示的非线性颜色空间。
以OpenGL作为图形库为例,Unity实现Gamma校正以及Linear workflow借助了OpenGL的 texture_sRGB 以及 framebuffer_sRGB 相关拓展,事实上是一种硬件层面的实现。
EXT_texture_sRGB会对texture做pow2.2的gamma校正,将输入从sRGB空间转换到线性空间;而framebuffer_sRGB如果开启,并且输出的目标是sRGB颜色空间,则硬件会将结果再做一次pow0.45(为了方便,下文使用0.45代替1/2.2)的gamma校正,将结果从线性空间转换到sRGB颜色空间。这样保证输入的是正确的数据,并且中间的计算是线性的过程,最后的结果再转移到sRGB空间来中和显示器的输出,这样就能保证一个正确的光照计算结果。
下面我们可以通过RenderDoc来分别分析一下gamma workflow和linear workflow在安卓手机上的渲染流水线。
不过我在使用RenderDoc的目前正式版本(v1.0 - 6 Mar, 2018)时遇到了一些小问题,即我无法正常的通过RenderDoc启动我的App,总是会报下图中的错误。
这其实是这个版本的一个bug,相关issue可以参考(https://github.com/baldurk/renderdoc/issues/903),解决方案的话就是不使用这个正式版,而是下载latest nightly build。
ok,回到正题。大家都知道,利用renderdoc我们可以很方便的查看渲染流水线的各种数据以及各种资源的参数等等,所以首先我们来看看在gamma空间下的整个工作流。
首先我在工程中倒入两张一样的图片,分别叫做gamma和linear,在gamma空间下一个勾选了导入设置中的sRGB,另一个则不勾选,并且都是用了ETC2的压缩格式。
可以看到两张图片并没有什么变化。下面我们来抓一帧来看一看两者在OpenGL中的纹理格式:
两者的格式都是GL_COMPRESSED_RGB8_ETC2。有趣吧,可以看到Unity设置了Gamma空间后,图片导入时无论是否选择勾选sRGB的结果都是一样的。
之后我们再来看一看FrameBuffer的情况:
没有什么意外,同样是我们常见且习惯的格式——GL_RGBA8。
这样整个gamma workflow的过程中没有涉及到所谓的gamma校正,整个过程和上一节中描述的一样——导入了经过处理的图片,最后再经过显示器的处理中和——传统且充满了巧合与错误。
接下来,我们将整个工程切换到Linear空间。同样,两张一样的图片一个勾选sRGB,另一个不勾选,并且同样是用了ETC2的压缩格式。
这次就更有趣了,我们可以看到勾选了sRGB的图片变暗了,而没有勾选的则仍然保持原样。并且,勾选sRGB的图片在下面的信息中显示是sRGB——它被作为一张sRGB纹理来看待,需要进行gamma校正;而另一张,则显示的是Linear——它被当作一张Linear纹理来看待,不需要经过gamma校正。
所以勾选了sRGB的纹理变的更暗了,这是因为经过了pow(2.2)的gamma校正处理。
下面我们来抓一帧来看一看在linear workflow下两者在OpenGL中的纹理格式:
可以看到,勾选了sRGB选项的Texture在OpenGL中的格式为GL_COMPRESSED_SRGB8_ETC2,即硬件会对其作一次Pow2.2的gamma校正,将它转化到线性空间中。
而没有勾选sRGB选项的Texture在OpenGL中的格式仍然是GL_COMPRESSED_RGB8_ETC2,所以硬件不会对它进行pow2.2的gamma校正操作,所以针对真正的线性空间图片不要勾选sRGB选项也就是这个原理——否则的话,颜色会比正确的结果更暗、数据也会错误。
不过有意思的还在后面,即framebuffer的格式。下面我们就来看一看framebuffer的抓帧结果:
framebuffer的格式为GL_SRGB8_ALPHA8,即此时保存的结果经过了pow0.45的gamma校正,从线性空间转换到了sRGB空间——这当然是合理的,因为它要中和最后显示器的gamma校正——但是,有一件事情这时会变的比较棘手......
0x03 透明混合
是的,这件事情就是透明混合的问题。由于透明混合是一个线性的过程,因此在混合中作为Dst的那一方的framebuffer的数据就要是线性空间的了。
所以此时混合的操作事实上会先将framebuffer的内容从sRGB空间再次pow2.2转换到线性空间,和src进行混合,再将混合后的结果pow0.45转换回sRGB空间保存到framebuffer中。
是不是有点乱?
我们来写一下公式,代入一个数据就明白了:
ret = (srcColor^2.2 * srcAlpha + dstColor^2.2 * (1 - srcAlpha) ) ^(1/2.2)
ok,这时我们假设src的color值的g分量为1,alpha为0.2;dst的color值的g分量为0。则计算结果为:
0.481156505。
但是在我们的传统的认知下,或者是在Gamma workflow的情况下,这次混合的结果是什么呢?我们来写一下混合公式:
ret = srcColor * srcAlpha + dstColor * (1 - srcAlpha)
代入同样的数据,计算的结果为:
0.2。
两个差别颇大的结果,而如果混合的次数越多,则结果的差别也会更大。
事实上这个问题的处理目前也并没有一个特别十全十美的解决方案,目前常见的几种做法大概包括以下几种方案:
- 使用gamma 1.0来制作资源,即在线性空间中制作资源。
- 自己在ui的shader中对alpha进行pow(2.2)的操作,但是这个只是稍微修复问题,并没有真正的解决问题。
- 仍然使用gamma space的workflow,但是涉及光照的shader自己来做pow 2.2和pow 0.45的校正。这样的话ui还在gamma space,而光照计算在linear space。
当然这里只是抛砖引玉,希望有更好解决方案的朋友能够在此多多分享一下经验。
Ref
http://www.klayge.org/2011/02/26/gamma的传说/
https://developer.nvidia.com/gpugems/GPUGems3
https://blog.csdn.net/candycat1992/article/details/46228771
https://renderdoc.org
https://www.khronos.org/registry/OpenGL/extensions/EXT/EXT_texture_sRGB.txt
https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_framebuffer_sRGB.txt
聊聊Unity的Gamma校正以及线性工作流的更多相关文章
- Gamma校正与线性工作流
1 Gamma校正是什么?8位亮度值x(0-1)经过x^0.45的一个提亮过程. 2 为什么需要Gamma校正 人的眼睛是以非线性方式感知亮度,在自然界中,人感觉到的一半亮度其实只有全部能量的0.2, ...
- 【转】关于LWF——线性工作流
1.什么是LWF? LWF全称Linear Workflow,中文翻译为线性工作流.“工作流”在这里可以当作工作流程来理解.LWF就是一种通过调整图像Gamma值,来使得图像得到线性化显示的技术流程. ...
- 浅谈unity中gamma空间和线性空间
转载请标明出处:http://www.cnblogs.com/zblade/ 一.概述 很久没有写文章了,今天写一篇对gamma空间和线性空间的个人理解总结,在查阅和学习了各个资料后,算是一个个人笔记 ...
- Gamma校正与线性空间
基础知识部分 为了方便理解,首先会对(Luminance)的相关概念做一个简单介绍.如果已经了解就跳到后面吧. 我们用Radiant energy(辐射能量)来描述光照的能量,单位是焦耳(J),因为光 ...
- 图像处理之gamma校正
1 gamma校正背景 在电视和图形监视器中,显像管发生的电子束及其生成的图像亮度并不是随显像管的输入电压线性变化,电子流与输入电压相比是按照指数曲线变化的,输入电压的指数要大于电子束的指数.这说明暗 ...
- gamma校正
1 gamma校正背景 在电视和图形监视器中,显像管发生的电子束及其生成的图像亮度并不是随显像管的输入电压线性变化,电子流与输入电压相比是按照指数曲线变化的,输入电压的指数要大于电子束的指数.这说明暗 ...
- OpenGL核心技术之Gamma校正
笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:<手把手教你/2.2次幂.Gamma校正后的暗红色就会成为(0.5,0.0 ...
- gamma校正原理
http://blog.csdn.net/u013286409/article/details/50239377 目录(?)[-] 图2中左图为原图,中图为gamma = 1/2.2在校正结果,原 ...
- Gamma校正及其OpenCV实现
參考:[1]http://www.cambridgeincolour.com/tutorials/gamma-correction.htm [2]http://en.wikipedia.org/wik ...
随机推荐
- (七十五)CoreLocation(一)在iOS7和iOS8设备上获取授权
苹果在iOS8上更新了CoreLocation的授权获取方式,在原来的基础上,不仅需要调用授权函数,还需要对info.plist进行相应的配置. 在iOS上获取经纬度使用的是CoreLocationM ...
- CSS House
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/ ...
- Learn Lua in 15 Minutes
原文地址:http://tylerneylon.com/a/learn-lua/ Learn Lua in 15 Minutes more or less For a more in-depth Lu ...
- web安全认证机制知多少
如今web服务随处可见,成千上万的web程序被部署到公网上供用户访问,有些系统只针对指定用户开放,属于安全级别较高的web应用,他们需要有一种认证机制以保护系统资源的安全,本文将探讨五种常用的认证机制 ...
- Mybatis逻辑分页原理解析RowBounds
Mybatis提供了一个简单的逻辑分页使用类RowBounds(物理分页当然就是我们在sql语句中指定limit和offset值),在DefaultSqlSession提供的某些查询接口中我们可以看到 ...
- Jeff Atwood质疑iPhone的单键设计
我喜欢使用iPhone,但我对它的一个设计不敢苟同:苹果始终坚持,设备的正面永远只能有一个按键. 我还买了一个Kindle Fire,它更离谱,一个按键都没有!我完全赞成,任何小器具的正面都应该在明显 ...
- Dynamics CRM 2013 停用和激活按钮的显示与隐藏
CRM中命令栏上的有些按钮是可以通过权限控制显示和隐藏的,比如新建.保存.保存并关闭.删除等,但惟独激活和停用无法控制,但我们还是可以用权限去控制,只是稍微绕了那么一下. 这里就要涉及到按钮的自定义了 ...
- 【Unity Shaders】Diffuse Shading——向Surface Shader添加properties
本系列主要参考<Unity Shaders and Effects Cookbook>一书(感谢原书作者),同时会加上一点个人理解或拓展. 这里是本书所有的插图.这里是本书所需的代码和资源 ...
- (NO.00001)iOS游戏SpeedBoy Lite成形记(八)
前一篇提到,如果要想动态修改选手的速度需要在update方法中添加代码. 因为update方法在游戏每一帧都会调用,所以我们不可能修改的太频繁.否则一来对性能有影响,而来玩家表现的极不自然,像抽风一样 ...
- LCS问题(最长公共子序列)-动态规划实现
问题描述: 问题] 求两字符序列的最长公共字符子序列 注意: 并不要求子串(字符串一)的字符必须连续出现在字符串二中. 思路分析: 最优子结构和重叠子问题的性质都具有,所以要采取动态规划的算法 最长公 ...