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校正以及线性工作流的更多相关文章

  1. Gamma校正与线性工作流

    1 Gamma校正是什么?8位亮度值x(0-1)经过x^0.45的一个提亮过程. 2 为什么需要Gamma校正 人的眼睛是以非线性方式感知亮度,在自然界中,人感觉到的一半亮度其实只有全部能量的0.2, ...

  2. 【转】关于LWF——线性工作流

    1.什么是LWF? LWF全称Linear Workflow,中文翻译为线性工作流.“工作流”在这里可以当作工作流程来理解.LWF就是一种通过调整图像Gamma值,来使得图像得到线性化显示的技术流程. ...

  3. 浅谈unity中gamma空间和线性空间

    转载请标明出处:http://www.cnblogs.com/zblade/ 一.概述 很久没有写文章了,今天写一篇对gamma空间和线性空间的个人理解总结,在查阅和学习了各个资料后,算是一个个人笔记 ...

  4. Gamma校正与线性空间

    基础知识部分 为了方便理解,首先会对(Luminance)的相关概念做一个简单介绍.如果已经了解就跳到后面吧. 我们用Radiant energy(辐射能量)来描述光照的能量,单位是焦耳(J),因为光 ...

  5. 图像处理之gamma校正

    1 gamma校正背景 在电视和图形监视器中,显像管发生的电子束及其生成的图像亮度并不是随显像管的输入电压线性变化,电子流与输入电压相比是按照指数曲线变化的,输入电压的指数要大于电子束的指数.这说明暗 ...

  6. gamma校正

    1 gamma校正背景 在电视和图形监视器中,显像管发生的电子束及其生成的图像亮度并不是随显像管的输入电压线性变化,电子流与输入电压相比是按照指数曲线变化的,输入电压的指数要大于电子束的指数.这说明暗 ...

  7. OpenGL核心技术之Gamma校正

    笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:<手把手教你/2.2次幂.Gamma校正后的暗红色就会成为(0.5,0.0 ...

  8. gamma校正原理

    http://blog.csdn.net/u013286409/article/details/50239377 目录(?)[-]   图2中左图为原图,中图为gamma = 1/2.2在校正结果,原 ...

  9. Gamma校正及其OpenCV实现

    參考:[1]http://www.cambridgeincolour.com/tutorials/gamma-correction.htm [2]http://en.wikipedia.org/wik ...

随机推荐

  1. J2EE进阶(十一)SSH框架整合常见问题汇总(二)

    org.hibernate.PropertyAccessException: IllegalArgumentException occurred while calling setter of cn. ...

  2. Android初级教程理论知识(第一章快速入门)

    一.综合介绍. Android项目的目录结构 Activity:应用被打开时显示的界面 src:项目代码 R.java:项目中所有资源文件的资源id Android.jar:Android的jar包, ...

  3. UNIX环境高级编程——select、poll和epoll

    一.select select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一. select的一个缺点在于单个进程能够监视的文件描述符的数量 ...

  4. ORACLE数据库 DBA常用知识

    <常用命令参考> 个系统变量值 SQL> show user --显示当前连接用户 SQL> show error --显示错误 SQL> set heading off ...

  5. 青年菜君与小农女送菜商业模式PK

    青年菜君与小农女送菜商业模式PK   对比项 青年菜君 小农女送菜 优势 劣势 开业 2014年3月3日 2013年9月 渠道 地铁捕获用户 写字楼配送 送货 来店面自取 送货到写字楼 菜君 1.减少 ...

  6. linux杀死进程的简单讲解

    一. 终止进程的工具kill .killall.pkill.xkill 终止一个进程或终止一个正在运行的程序,一般是通过kill .killall.pkill.xkill 等进行.比如一个程序已经死掉 ...

  7. Linux信号实践(4) --可靠信号

    Sigaction #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct si ...

  8. Html.java 存储页面信息类

    Html.java 存储页面信息类 package com.iteye.injavawetrust.miner; /** * 存储页面信息类 * @author InJavaWeTrust * */ ...

  9. 内连接、左外连接、右外连接、全外连接、交叉连接(CROSS JOIN)-----小知识解决大数据攻略

    早就听说了内连接与外连接,以前视图中使用过.这次自考也学习了,只是简单理解,现在深入探究学习(由于上篇博客的出现)与实践: 概念 关键字: 左右连接 数据表的连接有: 1.内连接(自然连接): 只有两 ...

  10. python 多窗口编辑

    同时打开多个文件: 1,vim filename1 filename2 在打开的多个文件中 :next 转到下个文件中 :prev 转到上个文件中 :last/:first 分别到最后一个和第一个文件 ...