
1. 项目概述从一次“颜色灾难”说起几年前我接手一个Unity项目美术同学在Photoshop里精心绘制了一套UI图标色彩鲜艳渐变过渡自然。但当他把切图导入Unity运行游戏后整个人都懵了——原本鲜亮的红色按钮变得暗淡发灰柔和的蓝色背景也显得脏兮兮的。他第一反应是“Unity是不是把我的图压缩坏了” 而我作为一个经历过类似“惨案”的程序立刻把目光投向了项目设置的“Color Space”选项。果然问题就出在“Linear”和“Gamma”这两个空间以及它们与PS默认的sRGB色彩模式之间那剪不断理还乱的关系上。这绝不是个例。无论是独立开发者还是团队协作只要涉及从PS或类似设计软件到Unity的视觉资源流转“颜色不对”几乎是一个必踩的坑。网上搜索“Unity颜色变暗”、“PS导入Unity色差”相关讨论铺天盖地但很多文章要么只讲操作“你勾上sRGB选项就行”要么大谈特谈色彩理论让人望而生畏。结果就是大家一直在“猜”这次该不该勾勾了为什么有时好有时坏Linear到底是个啥所以别再猜了。这篇文章的目的就是彻底讲透Unity的Linear色彩空间和Photoshop的sRGB色彩模式到底差在哪它们是如何“打架”的以及我们该如何用正确的“混合公式”让它们和平共处。我会抛开晦涩的学术定义用一个从业者解决实际问题的视角把原理、设置、排查和公式都掰开揉碎了讲清楚。无论你是程序、美术还是TA看完之后你不仅能解决眼前的色差问题更能理解背后的“道”从此告别盲目尝试。2. 核心概念拆解Linear与sRGB的本质区别要解决问题先得知道敌人在哪。Linear线性和sRGB根本不是同一个维度的概念把它们直接对比就像比较“公斤”和“米每秒”必须先统一语境。2.1 sRGB显示器的“方言”与存储的“压缩算法”首先sRGB首先是一个色彩空间标准它定义了红、绿、蓝三原色的色度坐标和白点确保在不同设备上看到的“红色”大致是同一个红色。我们日常接触的绝大多数显示器、网络图片JPEG, PNG、操作系统默认色彩管理都遵循sRGB。这是数字世界的“普通话”。其次也是造成混淆的关键sRGB定义了一种特定的“电光转换函数”EOTF通常被称为“Gamma校正”。这不是一个bug而是一个针对人类视觉系统和早期CRT显示器特性的优化设计。人类视觉特性我们对暗部细节的变化更敏感。用线性数据存储需要更多位数来精确描述暗部否则会出现色阶断裂。历史沿袭CRT显示器的物理特性其亮度与输入电压并非线性关系大致符合一个幂函数约2.2次方。于是sRGB标准巧妙地利用了这一点在存储和传输图片时对RGB值进行一个约0.45次方1/2.2的编码Gamma Encoding。这样做的结果是用更少的比特数如8位/通道就能在视觉上均匀分布亮度信息优化存储。图片数据被“压暗”存储。当显示器显示时它会自动施加一个约2.2次方的解码Gamma Decoding将数据“提亮”回线性光信号最终匹配人眼感知和显示器物理输出。所以你从PS导出的PNG、JPG文件里面的RGB值比如(255, 0, 0)不是线性的亮度值而是经过sRGB Gamma编码后的值。你可以把它理解为一种“压缩格式”目的是为了高效存储和传输。2.2 Linear线性物理计算的“通用语”与sRGB的“压缩存储”思维不同Linear空间指的是数值与实际物理光强呈线性正比关系的空间。在这里数值0.5代表的光强就是0.25的两倍是1.0的一半。这种关系简单、直接、符合物理规律。为什么在游戏渲染、特别是基于物理的渲染PBR中Linear空间至关重要光照计算真实世界的光照叠加是线性的。两盏相同的光照在一起亮度就是两倍。如果在非线性的sRGB空间做加法0.5 0.5的结果在视觉上并不是1.0会导致光照计算错误高光区域过曝或暗部细节丢失。颜色混合半透明混合Alpha Blending、模糊、色调映射等后期处理其数学基础都假设数据是线性的。在线性空间下执行这些操作结果才是物理正确的。纹理采样与过滤双线性、三线性过滤在线性空间下才能得到平滑、正确的过渡。在sRGB空间过滤会导致颜色偏移和暗纹。核心矛盾就此产生我们的源资源PS导出图是sRGB编码的“压缩包”而我们的渲染计算引擎Unity在Linear模式下需要Linear的“原始数据”。直接把这个“压缩包”丢进线性计算里就像把一首MP3歌曲的二进制数据直接当成乐谱来演奏结果必然是混乱的。注意Unity的Color Space设置中的Gamma并不是指sRGB而是指整个渲染管线都不进行色彩空间转换假设输入输出都是Gamma编码的。这是一种旧式、不精确但性能开销稍低的工作流。而Linear模式则是现代PBR渲染的标准它要求管线正确处理sRGB到Linear的转换。3. Unity中的色彩空间工作流与关键设置理解了本质区别我们来看Unity如何搭建桥梁。Unity的色彩空间工作流核心就是在正确的地方进行编解码。3.1 项目级设置选择你的“主战场”在Edit - Project Settings - Player - Other Settings中找到Color Space选项。这是最重要的总开关。Gamma (Legacy)逻辑Unity假设所有输入纹理颜色都是“已显示就绪”的Gamma空间数据。渲染计算也在这个非线性的Gamma空间中进行。最后直接输出给显示器。结果与PS中看到的效果接近因为避开了转换。但光照、混合计算不物理效果“假”特别是涉及复杂光照和半透明时。适用极老的项目、对性能极度敏感且视觉要求不高的移动端项目现已不推荐。Linear逻辑Unity假设输入纹理需要被转换。渲染计算在精确的Linear空间进行。最终输出前再转换回sRGBGamma空间以供显示器正确显示。结果渲染结果物理正确光照和颜色混合真实。但前提是输入纹理被正确标记和转换。适用所有现代项目尤其是使用URP/HDRP、PBR材质的项目。选择建议无脑选Linear。这是行业标准也是未来。3.2 纹理导入设置给资源贴上“身份标签”项目设为Linear后Unity不会自动知道哪张图是sRGB编码的如颜色贴图、UI图哪张图本来就是线性的如法线贴图、金属度贴图、粗糙度贴图等数据贴图。这就需要通过纹理的导入设置来告知引擎。在纹理的Import Settings面板中sRGB (Color Texture)这个复选框是解码开关。勾选 sRGB (Color Texture)含义告诉Unity“这张图是sRGB编码的颜色纹理请在采样时为我解码成Linear数据”。适用对象所有表示颜色的纹理。包括漫反射贴图/Albedo自发光贴图/Emission大多数UI精灵、图标、背景图任何在PS中制作、用于表现颜色的图片。不勾选 sRGB (Color Texture)含义告诉Unity“这张图的数据已经是线性的直接使用不要做sRGB解码”。适用对象所有表示物理属性数据的纹理。包括法线贴图/Normal Map金属度贴图/Metallic粗糙度贴图/Roughness/Smoothness环境光遮蔽贴图/AO高度图/Height Map遮罩贴图/Mask为什么法线贴图等不能勾sRGB因为法线向量-1到1被编码到颜色0到1中时其关系必须是线性的。如果被当成sRGB进行非线性解码向量的方向会被扭曲导致光照计算错误出现难看的凹凸错乱。3.3 一个典型的正确配置流程项目设置Color Space-Linear。纹理导入颜色类纹理从PS导出勾选sRGB (Color Texture)。数据类纹理从PS导出或程序生成不勾选sRGB (Color Texture)并确保纹理格式为RGB 24 bit等无损格式压缩格式可选Normal Quality。Shader中的颜色在Shader中如果你用Color或Vector属性在Inspector面板设置一个颜色值请注意在Linear项目下Inspector面板的颜色拾取器默认显示的是Gamma空间下的颜色为了让你直观选择。当你把这个颜色值传入Shader参与计算时Unity会自动在顶点片段着色器之前将其转换到Linear空间。所以在Shader代码中你通常直接使用这些颜色值进行线性计算即可。但如果你在代码中动态new Color(r,g,b)需要注意Color构造函数默认接受的是0-1的线性值。如果你从sRGB值转换需使用Color.LinearToGammaSpace和Color.GammaToLinearSpace进行手动转换。4. 混合公式详解从原理到Shader代码这是最硬核的部分但理解了它你就能真正驾驭颜色。我们常说的“混合”在渲染中主要指Alpha混合用于实现半透明效果。其公式在Linear和Gamma空间下截然不同。4.1 标准Alpha混合公式假设前景色当前片元为C_src(RGB) 和A_src(Alpha)背景色帧缓冲区已有颜色为C_dst(RGB)。标准混合模式为C_final C_src * A_src C_dst * (1 - A_src)这个公式成立的前提是C_src和C_dst处于同一个色彩空间且最好是Linear空间。4.2 Gamma空间下的错误混合如果我们的C_src来自sRGB纹理未正确解码和C_dst帧缓冲区可能已经是Gamma空间都在Gamma空间直接应用上述公式C_final_gamma C_src_gamma * A_src C_dst_gamma * (1 - A_src)由于Gamma空间的数值是非线性的这个加权平均在视觉上是不正确的。会导致混合边缘变暗、颜色浑浊。这就是为什么在旧式Gamma项目里半透明效果总感觉“脏”的原因。4.3 Linear空间下的正确混合手动版正确的流程是将sRGB编码的源颜色C_src_gamma解码到Linear空间C_src_linear DecodeSRGB(C_src_gamma)将帧缓冲区中的目标颜色C_dst_gamma假设显示器输出是Gamma的所以帧缓冲通常也是Gamma错在Linear项目下帧缓冲区可以是Linear的但最终输出会转换也统一到Linear空间。实际上在可编程渲染管线中我们可以也应该让混合在Linear空间进行。在Linear空间执行混合C_final_linear C_src_linear * A_src C_dst_linear * (1 - A_src)将混合结果编码回sRGB以供显示C_final_gamma EncodeSRGB(C_final_linear)Unity内置函数的帮助在Unity Shader中我们不需要手动实现编解码。Unity提供了内置宏和函数UNITY_COLORSPACE_GAMMA在编译时判断当前项目色彩空间。GammaToLinearSpace( color ),LinearToGammaSpace( color )进行空间转换。对于纹理采样使用tex2D(_MainTex, uv)时如果纹理导入设置勾选了sRGBUnity会自动进行解码返回Linear空间颜色。这是一个关键点4.4 一个实战Shader代码示例正确的半透明混合假设我们有一个简单的Unlit Shader用于渲染一张可能半透明的sRGB颜色纹理。Shader Custom/CorrectAlphaBlending { Properties { _MainTex (Texture, 2D) white {} _Color (Tint, Color) (1,1,1,1) // Inspector中选取的颜色Unity会处理转换 } SubShader { Tags { QueueTransparent RenderTypeTransparent IgnoreProjectorTrue } Blend SrcAlpha OneMinusSrcAlpha // 标准Alpha混合模式 ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include UnityCG.cginc struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; // 纹理缩放偏移 fixed4 _Color; v2f vert (appdata v) { v2f o; o.vertex UnityObjectToClipPos(v.vertex); o.uv TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { // 关键点1采样纹理。由于_MainTex在导入设置中勾选了sRGB // tex2D会自动返回Linear空间下的颜色值。 fixed4 col tex2D(_MainTex, i.uv); // 关键点2与Tint颜色相乘。_Color属性在Inspector中设置 // 在Linear项目下Unity传入Shader时已是Linear空间值。 // 因此这是Linear空间下的乘法。 col * _Color; // 此时col.rgb是Linear空间颜色col.a是透明度。 // 关键点3输出。我们使用了Blend SrcAlpha OneMinusSrcAlpha。 // 在Linear项目下帧缓冲区渲染目标如果配置正确期望Linear数据。 // 混合操作由硬件在片元输出后执行而我们的输出col是Linear的 // 背景色C_dst也应是Linear的来自上一轮渲染的正确输出 // 因此混合是在Linear空间执行的结果是正确的。 // 最终在写入显示缓冲区前Unity会整体执行Linear-sRGB转换。 return col; } ENDCG } } }这段代码的核心保障纹理设置正确sRGB勾选。项目空间是Linear。Shader中不对颜色做额外的非线性处理。混合模式设置正确。这样从纹理采样、颜色调制、到混合计算整个链路都在线性空间下完成确保了视觉正确性。5. 常见问题排查与实战心得理论很美好实践却总出岔子。下面是我总结的几个高频问题和排查思路。5.1 问题一颜色整体变暗/变亮症状在Unity中看到的整体色调与PS中明显不同不是局部错误而是全局性偏移。排查步骤检查项目Color Space确认是Linear还是Gamma。这是最根本的。检查纹理sRGB设置如果是Linear项目颜色纹理必须勾选sRGB。选中纹理在Inspector面板查看。一个快速筛选方法是在Project窗口搜索t:Texture2D sRGB:false找出所有未标记sRGB的纹理逐一判断是否为颜色贴图。检查显示设备确保你的显示器没有开启奇怪的“护眼模式”、“色彩增强”或HDR模式这些会影响你的判断。最好以sRGB模式校准的显示器为准。检查Player Settings在Player - Other Settings下确保Use RGB 32-bit Display Buffer在桌面平台被启用如果支持这能提供更高的颜色精度避免低位深带来的精度损失。5.2 问题二半透明混合边缘发黑/颜色不正症状半透明的UI叠加、粒子效果、透明材质边缘出现不自然的黑边或颜色浑浊。根本原因混合发生在错误的色彩空间。最常见的原因是混合的两端前景和背景色彩空间不一致。排查与解决确保渲染目标Render Texture/Frame Buffer是Linear的在URP/HDRP中检查你的渲染管线资产Pipeline Asset。对于URP确保Color Grading模式下的Tonemapping设置正确并且中间渲染纹理格式支持线性如R11G11B10或R16G16B16A16。自定义渲染纹理时创建RenderTexture时指定sRGB false对于Linear项目。检查Shader中的混合操作如第4部分所述确保参与混合的颜色都在Linear空间。避免在Shader中对采样后的颜色进行pow(color, 2.2)之类的硬编码Gamma校正。使用正确的Blend命令对于标准的Alpha混合Blend SrcAlpha OneMinusSrcAlpha在Linear空间下是正确的。避免使用Blend SrcAlpha One加法混合等模式来处理普通半透明除非你有特殊需求。注意渲染顺序错误的渲染顺序会导致背景颜色尚未准备好例如背景本身也是半透明且未渲染从而混合出错。合理设置物体的Render Queue。5.3 问题三法线贴图/金属粗糙度贴图效果错误症状法线贴图看起来凹凸方向不对或高光区域出现奇怪色块金属/粗糙度贴图控制的材质反射表现异常。根本原因数据类纹理被错误地标记为sRGB导致引擎对其进行了非线性的Gamma解码破坏了其线性数据。解决立即、马上、彻底检查所有法线贴图、金属度贴图、粗糙度贴图、AO贴图、高度图等数据贴图的导入设置取消勾选sRGB (Color Texture)。同时确保它们的压缩格式适合例如法线贴图使用Normal压缩格式。5.4 实战心得与技巧建立资源规范在团队中强制规定美术导出资源的命名或目录规范。例如所有颜色贴图放在Textures/Color文件夹所有数据贴图放在Textures/Data文件夹。然后可以编写一个简单的Editor脚本在导入时自动根据路径设置sRGB选项。善用预览工具在Unity编辑器中你可以通过Scene视图顶部的下拉菜单选择不同的渲染模式。切换到“Alpha”通道视图可以检查透明通道是否正确“Linear”或“Gamma”视图切换可以帮助你直观对比不同空间下的渲染结果但注意这只是在视图上的模拟不影响实际项目设置。后期处理与色调映射在Linear空间下工作最终输出到显示器前必须经过色调映射Tonemapping和sRGB编码。URP/HDRP的后期处理栈Post Processing默认会处理这些。如果你自己编写后处理Shader切记在最后一步使用LinearToGammaSpace()将结果转换或者确保渲染目标配置正确。UI的特殊性Unity UIuGUI/Canvas系统在Linear项目下默认会假设UI精灵纹理是sRGB并自动转换。但如果你在UI Shader中进行了复杂的自定义混合仍需遵循线性混合原则。对于全屏UI或需要与3D场景混合的UI要特别注意渲染目标和混合状态。性能考量Linear空间下的sRGB纹理采样和解码会有极微小的性能开销现代GPU上通常可忽略。真正的性能差异在于Gamma模式本身渲染不物理可能为了达到相似视觉效果需要更复杂的光照和后期来弥补总体而言Linear是现代且正确的选择。6. 高级话题HDR、ACES与更广的色域当你掌握了Linear与sRGB的基本工作流后可以进一步探索更前沿的色彩管理。HDR高动态范围Linear工作流是HDR的基础。HDR使用远超过[0,1]范围的浮点数值如FP16来表示亮度以容纳真实世界极大的明暗对比。在HDR管线中所有计算都在线性空间下进行最后通过色调映射将HDR结果压缩到显示器的LDR低动态范围范围内。ACES学院色彩编码系统一种行业标准的色彩空间和色调映射曲线常用于电影和3A游戏能提供更电影化、更一致的色彩表现。Unity的HDRP内置了对ACES的支持。ACES工作流同样基于线性空间只是在色调映射阶段使用了ACES标准的曲线。广色域如Display P3sRGB定义的色域相对较小。现代高端显示器支持更广的色域如Display P3。要利用广色域需要纹理包含颜色配置文件并在Unity中进行相应设置如使用Texture2D的colorSpace属性并在支持广色域的平台上启用。这比sRGB/Linear转换更复杂一层。对于大多数移动端和PC项目扎实掌握LinearsRGB工作流已经能解决99%的色彩问题。HDR和ACES是追求影视级画质的进阶方向。色彩管理不是魔法而是一套精确的工程规范。从PS到Unity本质是将艺术家在特定环境下定义的颜色通过一系列已知的、可逆的变换准确无误地还原到最终用户的屏幕上。理解Linear和sRGB就是理解这套变换规则的核心。希望这篇文章能成为你色彩管理工具箱里最趁手的那把螺丝刀下次当颜色再次“不对”时你能自信地说“我知道问题在哪也知道怎么修。”