
【USparkle專欄】如果你深懷絕技,愛“搞點(diǎn)研究”,樂于分享也博采眾長(zhǎng),我們期待你的加入,讓智慧的火花碰撞交織,讓知識(shí)的傳遞生生不息!
這是侑虎科技第1779篇文章,感謝作者七塊君供稿。歡迎轉(zhuǎn)發(fā)分享,未經(jīng)作者授權(quán)請(qǐng)勿轉(zhuǎn)載。如果您有任何獨(dú)到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群:793972859)
作者主頁:
https://www.zhihu.com/people/Na-Ka-4921
一、前言
自Unity支持Linear色彩空間以來,Unity的3D渲染的光影層次變得更加準(zhǔn)確細(xì)膩,但是Linear色彩空間下UI的渲染卻因此變得糟糕。絕大多數(shù)的UI資產(chǎn)都是在sRGB色彩空間里制作完成的,UI圖片的格式也都是基于sRGB,所以Linear色彩空間下,基于sRGB的UI圖片的Alpha得不到正確的混合,使得UI圖片的不透明度得不到正確的呈現(xiàn)。
為了匹配Linear色彩空間下的Alpha混合,有些團(tuán)隊(duì)限制使用半透明資產(chǎn);有些是在Ps里盲調(diào),再到Unity里做驗(yàn)證,直到看起舒服為止;或是在Ps里改變圖片色彩空間。這些做法無疑都是限制了UI制作流程。
現(xiàn)在的URP基于可編程渲染管線,支持自定義管線。要想徹底解決UI渲染問題,可以在Unity管線層面,做出合理的管線設(shè)計(jì),維持UI設(shè)計(jì)師正常的sRGB資產(chǎn)制作流程。
二、管線效果對(duì)比

左邊: Photoshop效果 | 中間: 自定義UI管線效果 | 右邊: Unity URP默認(rèn)效果
三、管線的設(shè)計(jì)思路

1. 在原有Linear色彩空間的Buffer里渲染3D圖形;
2. 將渲染完成的3D圖像轉(zhuǎn)移至Gamma色彩空間的UI Buffer中;
3. 在Gamma色彩空間的UI Buffer中繼續(xù)渲染UI圖片;
4. 將最終的渲染結(jié)果轉(zhuǎn)回到Linear,并最終輸出。
因?yàn)閁I圖片的Alpha Blend是在Gamma空間下完成的,所以不存在錯(cuò)誤的混合結(jié)果,兼容了Linear色彩空間的3D渲染和Gamma色彩空間下的UI渲染。
四、管線的具體實(shí)現(xiàn)
管線流程
思路有了,那么再結(jié)合URP現(xiàn)有的流程,詳細(xì)方案如下:

3D使用Base的Main Camera渲染,UI使用Overlay的UI Camera渲染,并把UI Camera塞到Main Camera的Stack當(dāng)中。

URP在使用Post-Processing(后處理)時(shí),本身在3D渲染完就會(huì)有一次Uber Post Process的Pass,以及不管有沒有后處理,在最終畫面渲染完都會(huì)有一次Final Blit的Pass(如果開了FXAA,則是Final Post)。只要我們?cè)谶@兩個(gè)Pass里做色彩空間轉(zhuǎn)換,幾乎不會(huì)產(chǎn)生多少額外的性能開銷。只有在不使用Post-Processing時(shí),才需要在3D渲染完成時(shí)額外補(bǔ)一個(gè)Pass做色彩空間轉(zhuǎn)換。
另外,Uber Post Process和Final Blit都會(huì)用到"Hidden/Universal Render Pipeline/Blit"這個(gè)Shader著色, 因此可以在Blit Shader的片元著色器里加入色彩空間轉(zhuǎn)換的函數(shù)和全局的Keyword宏,以方便在管線中利用Command Buffer設(shè)置keyword進(jìn)行色彩空間轉(zhuǎn)換。
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默認(rèn)渲染3D圖形的Buffer格式是RGBA111110Float:

如果在這個(gè)格式的Buffer里直接利用shader轉(zhuǎn)換色彩空間(LinearToSRGB)會(huì)造成3D圖像嚴(yán)重的色深精度丟失。所以需要一個(gè)和sRGB色彩信息契合的格式來儲(chǔ)存Shader轉(zhuǎn)換后色彩空間后的圖像信息,所以我選擇了RGBA32UNorm作為后續(xù)UI Buffer的格式,使用不同格式轉(zhuǎn)換色彩后的色深對(duì)比如下:

可以看出來,使用RGBA32UNorm能更好地儲(chǔ)存轉(zhuǎn)換后的色深精度。
轉(zhuǎn)換Buffer的方法,我這里未在ForwardRenderer里為UI重新聲明創(chuàng)建RT,以及根據(jù)是否是UI相機(jī),讓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)里,依據(jù)是否畫UI來重新設(shè)置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后面會(huì)講到。
UI的分辨率
因?yàn)樵?D物體渲染完畢時(shí)切換了Buffer,在這里有一次全面重置Buffer尺寸的機(jī)會(huì),我們可以修改接下來UI Buffer的分辨率,以達(dá)到即使降低3D的渲染質(zhì)量,也依然能保證UI以滿屏幕分辨率渲染。
在前面ForwardRenderer的CreateCameraRenderTarget()方法里創(chuàng)建UI RT時(shí)指定新的寬高尺寸:
var uiDescriptor = descriptor; uiDescriptor.useMipMap = false; uiDescriptor.autoGenerateMips = false; uiDescriptor.depthBufferBits = 24; uiDescriptor.height = Screen.height; /* 設(shè)置 UI Render Target 的高度 */ uiDescriptor.width = Screen.width; /* 設(shè)置 UI Render Target 的寬度 */ uiDescriptor.graphicsFormat = GraphicsFormat.R8G8B8A8_UNorm; cmd.GetTemporaryRT(m_UguiTaget.id, uiDescriptor, FilterMode.Bilinear);

UGUI Pass
Unity默認(rèn)情況是UI在DrawTransparentObjects Pass里繪制,Game視圖因?yàn)橛歇?dú)立的UI相機(jī),所以問題不大,但是Scene視圖里只有一個(gè)相機(jī),借助Render Doc可以看出UI和一般的半透明物體是混在一起的,想在UI和半透明物體之間插入自定義Pass是不可能的。理想狀態(tài)是:讓UI擁有屬于自己的Pass,方便后期維護(hù)管理。
在Forward Renderer中單獨(dú)聲明了一個(gè)DrawOjectsPass類型的UGUI Pass,構(gòu)造如下:
m_UguiPass = new DrawObjectsPass("UGUI", false,
RenderPassEvent.BeforeRenderingTransparents +1,
RenderQueueRange.transparent,
LayerMask.GetMask("UI"), m_DefaultStencilState,
stencilData.stencilReference);
用指定的Layer Mask ("UI")來作為這個(gè)Pass的渲染條件。使用"UI" layer的Transparent序列物體都會(huì)進(jìn)入這個(gè)Pass。

注:不能忘了重新配置Forward Renderer Data, 要把"UI" Layer Mask在Transparent Layer Mask中去掉,否則UI會(huì)被DrawTransparentObjects和UGUI這兩個(gè)Pass重復(fù)繪制。

UI和半透明物體分離后,Scene視圖也可以方便的校色了。

UI圖片的色彩空間
在管線修復(fù)后,理論上UI圖片就不用勾選sRGB了,但是,我在這里做法是:UI圖片維持勾選sRGB,并在UI的Shader里反向矯正回打勾前的狀態(tài)。

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用一個(gè)全局變量"_IsInUICamera"為mask進(jìn)行Lerp插值,全局變量"_IsInUICamera"在DrawObjectPass中實(shí)時(shí)全局賦值:
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相機(jī)渲染階段走sRGB的渲染流程,非UI相機(jī)一切照舊,規(guī)避掉因?yàn)閁I校色而導(dǎo)致非UI相機(jī)渲染的UI圖片(*例如:世界空間下的UI)顏色不正確。
注:即UI相機(jī)中UI的顏色和不透明度都是正確的,非UI相機(jī)中的UI顏色正確但不透明度未被矯正。所以暫時(shí)不支持非UI相機(jī)的不透明度矯正,但也不影響正常使用。
重置UI組件默認(rèn)Shader
UI組件,比如Image,在不使用自定義材質(zhì)時(shí),會(huì)默認(rèn)使用Shader "UI/Defaut",而這個(gè)Shader是內(nèi)置不可編輯的,對(duì)于我們使用了自定的UI Shader和后期對(duì)Shader框架進(jìn)行擴(kuò)展就很不方便,我們需要UI組件默認(rèn)使用我們自己寫的Shader。

UI 組件 默認(rèn)著色器
好在UI組件的源碼是可以編輯的,順著Image組件的源碼,可以看到Image類繼承了MaskableGraphic類,MaskableGraphic類又繼承了Graphic類,這個(gè)就是UI組件的根源了。可以看到Graphic類里有一段設(shè)置默認(rèn)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; } }
這樣默認(rèn)UI Shader就替換成我們自己的了,后面就可以愉快地搭建UI Shader框架了。

修改后 的默認(rèn)UI Shader變成了我們的自定義著色器
五、尾聲
最后的最后,是我項(xiàng)目的GitHub地址:
https://github.com/TakeshiCho/UI_RenderPipelineInLinearSpace
文末,再次感謝 七塊君 的分享, 作者主頁: https://www.zhihu.com/people/Na-Ka-4921, 如果您有任何獨(dú)到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群: 793972859 )。
近期精彩回顧
熱門跟貼