一定要在线性空间(Linear Space)中做光照计算

本文深入探讨了Gamma校正的概念及其在游戏渲染中的应用。解释了为什么需要在光照计算前进行GammaDecode,并展示了在正确和错误的Gamma处理方式下渲染效果的区别。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

啥是Gamma Correction?什么是在线性空间(Linear Space)中做光照计算?

线性空间这玩意已经说烂了,不过再烂也不是生而知之,该学还得学,这里总结一下。

先介绍下啥是Gamma(别急,先看下去,这是解释线性空间的前置知识)。这词儿N多人听说过,而且也被各种滥用。这里只解释游戏即时渲染相关的概念。首先,就是老式CRT显示器的一个问题,给显示器输入的电压和输出的亮度不成线性关系,啥意思呢,就是你告诉显示器我这个像素颜色是0.5,显示器给个亮度,然后你说下个像素颜色是1.0,然后显示器给的亮度不是上一个亮度的两倍,而是成下面这个函数的关系:
Y = pow(X, Gamma)
也就是输出是输入的Gamma次方。而一般来说这个Gamma值是2.2(具体值根据各种标准不太一样,这里统一认为是2.2)

红色的曲线就是Y = pow(X, 2.2)的曲线,可以看出,显示器输出的图片是会比想象中的暗的。那么咋办呢,各种图片,视频等其实在进行数据储存的时候,都会做Gamma Encode(也会被称为Gamma Correction,这个词可能会被用到各种地方,包括Gamma Decode也会被称为Gamma Correction,为了防止混乱,这里就不这么叫了干脆)。举个例子,照相的时候,你相机照下照片后是会做一系列后处理,然后再存到SD卡里的,其中有一项后处理就是Gamma Encode,也就是将整体画面“调亮”,然后显示器再显示的时候,再做一次Gamma Decode,这样显示的画面就Gamma抵消了,亮度刚好OK。(你应该注意到高级相机都会可能储存raw这个格式的图片,超级大,这种图片是没有做Gamma Encode的,就是纯记录下来的原始数据。那么显示为啥还正确呢,是因为大部分能打开raw文件的软件,都知道你没做Gamma Encode,所以软件会在显示图片前给Encode一下)。虽然现代的LCD显示器已经没有自带的Gamma Decode“功能”了,但是为了兼容以前的显示器,还是会调一个Gamme Decode出来。

除此以外,各种图片视频会做Gamma Encode的原因还有一个,就是人眼识别光线亮暗的敏感度是不一样的,人眼在看暗处的时候会比看亮的更加敏感。而由于图片一般使用8bit每color channel,也就是所谓的24位真彩色,这个储存数据如果平均分配亮暗程度的话,那么给暗处的数据量会不够,人眼会看出明显“色带”问题,而如果做了Gamma Encode的话,刚刚好(这可真是凑巧)是把暗处信息储存更多,而且存储的比例也刚好差不多符合人眼对于亮暗敏感程度的比例,这种存储的图像空间就是耳熟能详的sRGB图像空间。
下图就是如果用Linear Encode的话,显示是什么样子的,为了更加显眼,用的是32阶的5bit储存。注意看暗部细节,明显不够用,而Gamma Encode则是亮暗部很均衡。

Linear Encode in 5 bits

Gamma Encode in 5 bits

说了半天都是在讲Gamma相关的东西,现在终于说到和游戏相关的了,其实可能已经猜到了,传统渲染流程中根本就没有处理Gamma这个东西,是下面这个流程:

这个流程是错的,因为直接使用了Encoded Gamma图去做光照计算,也就是说,使用了非线性的输入来进行线性计算(shader中计算光照可都是线性的),出来的结果自然是错误的。正确做法是先把图片转到线性空间(Gamma Decode),然后进行光照计算,然后再把计算结果做Gamma Encode,再输出给屏幕,也就是下面这个流程图:

现在对比下两者的区别,上面的是错误的,下面是正确的。可以看出,由于计算错误,高光部分已经奇怪的从白色偏移到黄绿色了。我知道很多人觉得上面那个阴影过度很好看,但是如果你观察现实的话,你会发现现实中的过度边界就是下面那么“硬”,如下图:

如果你喜欢错误的那种阴影过度样式,也应该去调整shader专门做相关渲染,而不是用一个错误结果的副产品将就了。

而哪些贴图需要提前做Gamma Decode呢,原则上来说是Diffuse贴图。而Normal, Metal, Roughness等只是拿贴图储存数据,而不是颜色的贴图则不要做Gamma Decode。

参考:
<think>我们正在讨论Unity中的颜色空间转换问题。用户希望了解如何在Gamma空间下实现Linear空间的视觉效果。根据引用[1],我们知道sRGB到线性空间的转换涉及伽马校正:低亮度区域(RGB/12.92),高亮度区域((RGB+0.055)/1.055)^2.4)。引用[2]提到了在OpenGL中加载纹理时,如果纹理是sRGB格式,应该使用GL_SRGB8内部格式,这样OpenGL会自动进行转换。引用[3]则提到了Unity中Shader的BRDF计算,但具体内容没有展开。用户的问题:在Gamma空间下实现Linear空间的视觉效果。分析:Unity有两种颜色空间:GammaLinear。在Linear空间下,光照计算是线性的,更符合物理规律。而Gamma空间下,图像存储时已经经过伽马编码(近似幂函数2.2的倒数),显示时显示器会进行伽马解码(幂函数2.2)以正确显示。如果项目因为某些原因必须使用Gamma空间(例如移动平台不支持Linear空间),但又希望得到Linear空间的视觉效果(即光照计算是线性的),那么我们需要手动进行转换。解决方案:1.在Gamma空间下,我们可以通过Shader手动进行线性化(即伽马校正的逆过程)来模拟线性空间光照计算。2.具体步骤:a.将纹理从sRGB转换到线性空间(如果纹理是sRGB格式)。b.在线性空间下进行光照计算。c.将计算结果再转换回Gamma空间(因为输出到显示器需要经过伽马编码)。注意:在Gamma空间下,Unity默认不会对纹理进行任何转换(即纹理保持原样),所以我们需要在Shader中手动转换。步骤详解:1.纹理转换(sRGB到线性空间):根据引用[1],我们可以使用以下函数将sRGB颜色转换为线性颜色:```float3sRGBToLinear(float3c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);}```但实际上,由于性能考虑,通常使用近似的2.2次幂(因为2.4计算较慢):```float3sRGBToLinear(float3c){returnpow(c,2.2);}```或者更精确的:```float3sRGBToLinear(float3c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);}```2.光照计算:在获取了线性空间的颜色后,进行光照计算(例如漫反射、镜面反射等),这些计算都是在线性空间下进行的。3.将计算结果转换回Gamma空间(线性到sRGB):```float3LinearTosRGB(float3c){return(c<=0.0031308)?c*12.92:1.055*pow(c,1.0/2.4)-0.055;}```同样,也可以使用近似的2.2次幂的倒数:```float3LinearTosRGB(float3c){returnpow(c,1.0/2.2);}```4.在Shader中,我们通常需要在片元着色器中进行这些转换:-对于输入纹理(如漫反射贴图),如果它在导入时被标记为sRGB(默认情况下,颜色纹理会被标记为sRGB),那么在Gamma空间下,Unity不会进行转换,因此我们需要在Shader中手动转换为线性空间。-进行光照计算线性空间)。-将最终颜色转换回Gamma空间。但是,注意:在Gamma空间下,Unity的灯光颜色和材质颜色也是以sRGB形式存在的,因此我们可能也需要将这些输入转换为线性空间。然而,这种法会增加Shader的复杂性,并且可能影响性能。另一种替代方案是使用Unity提供的内置函数:-`GammaToLinearSpace`和`LinearToGammaSpace`(在UnityCG.cginc中定义)在UnityCG.cginc中,我们可以找到这两个函数。因此,我们可以这样写:示例代码:float3diffuseColor=tex2D(_MainTex,uv).rgb;//将纹理颜色从Gamma空间转换到线性空间diffuseColor=GammaToLinearSpace(diffuseColor);//同样,如果使用灯光颜色,也需要转换(但Unity在Gamma空间下提供的灯光颜色是Gamma空间的,所以需要转换)float3lightColorLinear=GammaToLinearSpace(_LightColor0.rgb);//然后进行线性空间光照计算(例如Lambert)float3diffuse=lightColorLinear*diffuseColor*saturate(dot(normal,lightDir));//其他光照计算(如高光)...//将最终结果转换回Gamma空间float3finalColor=LinearToGammaSpace(diffuse+specular);注意:这种方法会使得整个光照计算线性空间进行,从而在Gamma空间下模拟出Linear空间的视觉效果。但是,这种手动转换并不是完美的,因为:-所有输入(如灯光、材质颜色、纹理)都需要转换,容易遗漏。-性能开销增加。另外,如果项目中有后处理效果,也需要考虑颜色空间转换。更推荐的法是:如果目标平台支持,尽量使用Linear空间。只有在不支持的情况下才使用这种方法。总结步骤:1.在Unity的PlayerSettings中,设置颜色空间为Gamma。2.在Shader中,对输入的纹理和颜色进行GammaToLinearSpace转换。3.在线性空间下进行光照计算。4.输出前,将最终颜色转换回Gamma空间(LinearToGammaSpace)。注意:Unity内置的GammaToLinearSpaceLinearToGammaSpace函数已经封装了标准的转换公式(使用近似2.2次幂)。参考文献:[^1]提供了sRGB到线性空间的转换公式。[^2]提到了在OpenGL中处理sRGB纹理的方法,但在Gamma空间下Unity不会自动转换,所以需要手动处理。相关问题:1.为什么Gamma空间下光照计算是非线性的?2.Unity中如何判断当前的颜色空间?3.除了在Shader中手动转换,还有哪些方法可以在Gamma空间下模拟线性空间的效果?</think>### 在Gamma空间下实现Linear空间视觉效果的方法 在Unity的Gamma空间下模拟Linear空间的视觉效果,需要通过**手动伽马校正**来解决颜色计算的非线性问题。核心思路是:在Shader中对输入纹理进行线性化处理,光照计算完成后执行反向伽马编码。具体步骤如下: #### 1. 纹理输入处理(sRGB → 线性空间) ```glsl // 在片元着色器中转换纹理颜色 float3 sRGBToLinear(float3 c) { return (c <= 0.04045) ? c / 12.92 : pow((c + 0.055)/1.055, 2.4); } void surf (Input IN, inout SurfaceOutputStandard o) { float3 baseColor = tex2D(_MainTex, IN.uv_MainTex).rgb; float3 linearColor = sRGBToLinear(baseColor); // 转换为线性空间 o.Albedo = linearColor; } ``` #### 2. 光照计算(在线性空间执行) 保持光照计算不变(Unity在Gamma空间下会按Gamma计算,但此时输入已是线性值): ```glsl // 标准光照模型会自动处理 o.Albedo *= _LightColor0.rgb * saturate(dot(IN.worldNormal, _WorldSpaceLightPos0.xyz)); ``` #### 3. 输出处理(线性空间 → sRGB) ```glsl // 最终输出前转换回Gamma空间 float3 LinearTosRGB(float3 c) { return (c <= 0.0031308) ? c * 12.92 : 1.055 * pow(c, 1.0/2.4) - 0.055; } void surf (Input IN, inout SurfaceOutputStandard o) { ... o.Albedo = LinearTosRGB(o.Albedo); // 写回Gamma空间 } ``` #### 4. 关键设置 - **纹理导入**:在Inspector中取消勾选`sRGB (Color Texture)`,避免Unity自动转换 - **颜色空间**:`Edit > Project Settings > Player` 中保持`Color Space = Gamma` - **灯光强度**:适当提高光源强度(Gamma空间下亮度感知不同) #### 注意事项 1. 性能影响:手动转换会增加GPU计算量,移动平台建议使用近似公式: ```glsl float3 approxLinear = pow(baseColor, 2.2); // 简化sRGB→Linear float3 approxGamma = pow(resultColor, 1/2.2); // 简化Linear→sRGB ``` 2. 后处理兼容性:所有后处理Shader需同步实现颜色空间转换 3. HDR内容:HDR颜色需先Tone Mapping再转换,避免过曝 > 此方案通过手动模拟线性工作流,解决了Gamma空间下光照混合非线性问题[^1]。实际效果接近Linear空间,但物理准确性仍略低于原生线性空间[^2]。 --- ### 相关问题 1. **为什么Unity的Linear空间需要特定硬件支持?** 2. **如何在Shader中自动检测当前颜色空间(Gamma/Linear)?** 3. **Gamma空间下PBR材质(如Standard Shader)会出现哪些视觉异常?** 4. **手动伽马校正对移动端性能的影响如何量化?** 5. **除Shader修改外,还有哪些方法优化Gamma空间下的颜色准确性?**
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值