基于URP搭建Linear色彩空间下的UI渲染管线
- 作者:admin
- /
- 时间:12小时前
- /
- 浏览:41 次
- /
- 分类:厚积薄发
【USparkle专栏】如果你深怀绝技,爱“搞点研究”,乐于分享也博采众长,我们期待你的加入,让智慧的火花碰撞交织,让知识的传递生生不息!
一、前言
自Unity支持Linear色彩空间以来,Unity的3D渲染的光影层次变得更加准确细腻,但是Linear色彩空间下UI的渲染却因此变得糟糕。绝大多数的UI资产都是在sRGB色彩空间里制作完成的,UI图片的格式也都是基于sRGB,所以Linear色彩空间下,基于sRGB的UI图片的Alpha得不到正确的混合,使得UI图片的不透明度得不到正确的呈现。
为了匹配Linear色彩空间下的Alpha混合,有些团队限制使用半透明资产;有些是在Ps里盲调,再到Unity里做验证,直到看起舒服为止;或是在Ps里改变图片色彩空间。这些做法无疑都是限制了UI制作流程。
现在的URP基于可编程渲染管线,支持自定义管线。要想彻底解决UI渲染问题,可以在Unity管线层面,做出合理的管线设计,维持UI设计师正常的sRGB资产制作流程。
二、管线效果对比

左边: Photoshop效果 | 中间: 自定义UI管线效果 | 右边: Unity URP默认效果
三、管线的设计思路

- 在原有Linear色彩空间的Buffer里渲染3D图形;
- 将渲染完成的3D图像转移至Gamma色彩空间的UI Buffer中;
- 在Gamma色彩空间的UI Buffer中继续渲染UI图片;
- 将最终的渲染结果转回到Linear,并最终输出。
因为UI图片的Alpha Blend是在Gamma空间下完成的,所以不存在错误的混合结果,兼容了Linear色彩空间的3D渲染和Gamma色彩空间下的UI渲染。
四、管线的具体实现
管线流程
思路有了,那么再结合URP现有的流程,详细方案如下:

3D使用Base的Main Camera渲染,UI使用Overlay的UI Camera渲染,并把UI Camera塞到Main Camera的Stack当中。

URP在使用Post-Processing(后处理)时,本身在3D渲染完就会有一次Uber Post Process的Pass,以及不管有没有后处理,在最终画面渲染完都会有一次Final Blit的Pass(如果开了FXAA,则是Final Post)。只要我们在这两个Pass里做色彩空间转换,几乎不会产生多少额外的性能开销。只有在不使用Post-Processing时,才需要在3D渲染完成时额外补一个Pass做色彩空间转换。
另外,Uber Post Process和Final Blit都会用到"Hidden/Universal Render Pipeline/Blit"这个Shader着色, 因此可以在Blit Shader的片元着色器里加入色彩空间转换的函数和全局的Keyword宏,以方便在管线中利用Command Buffer设置keyword进行色彩空间转换。
Shader("Hidden/Universal Render Pipeline/Blit"):
half4 Fragment(Varyings input) : SV_Target
{
UNITY_SETUP_STEREO_EYE_INDEX_POST_VERTEX(input);
half4 col = SAMPLE_TEXTURE2D_X(_SourceTex, sampler_SourceTex, input.uv);
#ifdef _LINEAR_TO_SRGB_CONVERSION
col = LinearToSRGB(col);
#endif
#ifdef _SRGB_TO_LINEAR_CONVERSION
col = SRGBToLinear(col);
#endif
return col;
}
管线部分(Uber Post Process):
var cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, m_ProfilingRenderPostProcessing))
{
cmd.EnableShaderKeyword(ShaderKeywordStrings.LinearToSRGBConversion);
Render(cmd, ref renderingData);
cmd.DisableShaderKeyword(ShaderKeywordStrings.LinearToSRGBConversion);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
3D Buffer和UI Buffer的格式:
值得注意的是,Unity默认渲染3D图形的Buffer格式是RGBA111110Float:

如果在这个格式的Buffer里直接利用shader转换色彩空间(LinearToSRGB)会造成3D图像严重的色深精度丢失。所以需要一个和sRGB色彩信息契合的格式来储存Shader转换后色彩空间后的图像信息,所以我选择了RGBA32UNorm作为后续UI Buffer的格式,使用不同格式转换色彩后的色深对比如下:

可以看出来,使用RGBA32UNorm能更好地储存转换后的色深精度。
转换Buffer的方法,我这里未在ForwardRenderer里为UI重新声明创建RT,以及根据是否是UI相机,让Final Blit Pass选择接受不同的Render Target:
RenderTargetHandle m_UguiTaget;
......
m_UguiTaget.Init("_UIColorTexture");
......
void CreateCameraRenderTarget(ScriptableRenderContext context, ref RenderTextureDescriptor descriptor, bool createColor, bool createDepth)
{
......
{
var uiDescriptor = descriptor;
uiDescriptor.useMipMap = false;
uiDescriptor.autoGenerateMips = false;
uiDescriptor.depthBufferBits = 24;
uiDescriptor.graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm;
cmd.GetTemporaryRT(m_UguiTaget.id, uiDescriptor, FilterMode.Bilinear);
}
......
}
......
public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{
......
if (!cameraTargetResolved)
{
RenderTargetHandle finalTarget = isUICamera ? m_UguiTaget : m_ActiveCameraColorAttachment;
m_FinalBlitPass.Setup(cameraTargetDescriptor, finalTarget);
EnqueuePass(m_FinalBlitPass);
}
......
}
并在UGUI Pass(DrawObjectsPass)里,依据是否画UI来重新设置Render Target:
/* Add by: Takeshi, Set UI Render target */
if (m_IsGameViewUI && m_UguiTarget != default)
{
cmd.SetRenderTarget(m_UguiTarget.Identifier());
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
}
/* End Add */
context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref filterSettings, ref m_RenderStateBlock);
注:UGUI Pass后面会讲到。
UI的分辨率
因为在3D物体渲染完毕时切换了Buffer,在这里有一次全面重置Buffer尺寸的机会,我们可以修改接下来UI Buffer的分辨率,以达到即使降低3D的渲染质量,也依然能保证UI以满屏幕分辨率渲染。
在前面ForwardRenderer的CreateCameraRenderTarget()方法里创建UI RT时指定新的宽高尺寸:
var uiDescriptor = descriptor;
uiDescriptor.useMipMap = false;
uiDescriptor.autoGenerateMips = false;
uiDescriptor.depthBufferBits = 24;
uiDescriptor.height = Screen.height; /* 设置 UI Render Target 的高度 */
uiDescriptor.width = Screen.width; /* 设置 UI Render Target 的宽度 */
uiDescriptor.graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm;
cmd.GetTemporaryRT(m_UguiTaget.id, uiDescriptor, FilterMode.Bilinear);

UGUI Pass
Unity默认情况是UI在DrawTransparentObjects Pass里绘制,Game视图因为有独立的UI相机,所以问题不大,但是Scene视图里只有一个相机,借助Render Doc可以看出UI和一般的半透明物体是混在一起的,想在UI和半透明物体之间插入自定义Pass是不可能的。理想状态是:让UI拥有属于自己的Pass,方便后期维护管理。
在Forward Renderer中单独声明了一个DrawOjectsPass类型的UGUI Pass,构造如下:
m_UguiPass = new DrawObjectsPass("UGUI", false,
RenderPassEvent.BeforeRenderingTransparents +1,
RenderQueueRange.transparent,
LayerMask.GetMask("UI"), m_DefaultStencilState,
stencilData.stencilReference);
用指定的Layer Mask ("UI")来作为这个Pass的渲染条件。使用"UI" layer的Transparent序列物体都会进入这个Pass。

注:不能忘了重新配置Forward Renderer Data, 要把"UI" Layer Mask在Transparent Layer Mask中去掉,否则UI会被DrawTransparentObjects和UGUI这两个Pass重复绘制。

UI和半透明物体分离后,Scene视图也可以方便的校色了。

UI图片的色彩空间
在管线修复后,理论上UI图片就不用勾选sRGB了,但是,我在这里做法是:UI图片维持勾选sRGB,并在UI的Shader里反向矫正回打勾前的状态。

UI Shader的Fragment:
float4 pixel(v2f IN) : SV_Target
{
......
half4 color;
......
color.rgb = lerp(color.rgb,LinearToSRGB(color.rgb),_IsInUICamera);
// "One OneMinusSrcAlpha".
color.rgb *= color.a;
return color;
}
将Linear的Color和sRGB的Color用一个全局变量"_IsInUICamera"为mask进行Lerp插值,全局变量"_IsInUICamera"在DrawObjectPass中实时全局赋值:
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
// NOTE: Do NOT mix ProfilingScope with named CommandBuffers i.e. CommandBufferPool.Get("name").
// Currently there's an issue which results in mismatched markers.
CommandBuffer cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, m_ProfilingSampler))
{
Camera camera = renderingData.cameraData.camera;
if (camera.CompareTag("UICamera"))
cmd.SetGlobalFloat(ShaderPropertyId.isInUICamera,1);
#if UNITY_EDITOR
else if(m_FilteringSettings.layerMask == LayerMask.GetMask("UI") && renderingData.cameraData.isSceneViewCamera)
cmd.SetGlobalFloat(ShaderPropertyId.isInUICamera,1);
#endif
else cmd.SetGlobalFloat(ShaderPropertyId.isInUICamera,0);
......
}
......
}
这样确保只在UI相机渲染阶段走sRGB的渲染流程,非UI相机一切照旧,规避掉因为UI校色而导致非UI相机渲染的UI图片(*例如:世界空间下的UI)颜色不正确。
注:即UI相机中UI的颜色和不透明度都是正确的,非UI相机中的UI颜色正确但不透明度未被矫正。所以暂时不支持非UI相机的不透明度矫正,但也不影响正常使用。
重置UI组件默认Shader
UI组件,比如Image,在不使用自定义材质时,会默认使用Shader "UI/Defaut",而这个Shader是内置不可编辑的,对于我们使用了自定的UI Shader和后期对Shader框架进行扩展就很不方便,我们需要UI组件默认使用我们自己写的Shader。

UI组件默认着色器
好在UI组件的源码是可以编辑的,顺着Image组件的源码,可以看到Image类继承了MaskableGraphic类,MaskableGraphic类又继承了Graphic类,这个就是UI组件的根源了。可以看到Graphic类里有一段设置默认UI Shader的代码。

修改一下:
static public Material defaultGraphicMaterial
{
get
{
// Find Custom UI Shader
Shader uiShader = Shader.Find("UI/URP_Linear_Space_Default");
Material uiMaterial = new Material(uiShader);
if (s_DefaultUI == null)
//s_DefaultUI = Canvas.GetDefaultCanvasMaterial();
s_DefaultUI = uiMaterial;
return s_DefaultUI;
}
}
这样默认UI Shader就替换成我们自己的了,后面就可以愉快地搭建UI Shader框架了。

修改后的默认UI Shader变成了我们的自定义着色器
五、尾声
最后的最后,是我项目的GitHub地址:
https://github.com/TakeshiCho/UI_RenderPipelineInLinearSpace
这是侑虎科技第1779篇文章,感谢作者七块君供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)
作者主页:https://www.zhihu.com/people/Na-Ka-4921
再次感谢七块君的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)