打開網(wǎng)易新聞 查看精彩圖片

【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ì)比

打開網(wǎng)易新聞 查看精彩圖片

左邊: Photoshop效果 | 中間: 自定義UI管線效果 | 右邊: Unity URP默認(rèn)效果

三、管線的設(shè)計(jì)思路

打開網(wǎng)易新聞 查看精彩圖片

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ì)方案如下:

打開網(wǎng)易新聞 查看精彩圖片

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

打開網(wǎ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:

打開網(wǎng)易新聞 查看精彩圖片

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

打開網(wǎng)易新聞 查看精彩圖片

可以看出來,使用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);

打開網(wǎng)易新聞 查看精彩圖片

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。

打開網(wǎng)易新聞 查看精彩圖片

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

打開網(wǎng)易新聞 查看精彩圖片

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

打開網(wǎng)易新聞 查看精彩圖片

UI圖片的色彩空間

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

打開網(wǎng)易新聞 查看精彩圖片

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。

打開網(wǎng)易新聞 查看精彩圖片

UI 組件 默認(rèn)著色器

好在UI組件的源碼是可以編輯的,順著Image組件的源碼,可以看到Image類繼承了MaskableGraphic類,MaskableGraphic類又繼承了Graphic類,這個(gè)就是UI組件的根源了。可以看到Graphic類里有一段設(shè)置默認(rèn)UI Shader的代碼。

打開網(wǎng)易新聞 查看精彩圖片

修改一下:

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框架了。

打開網(wǎng)易新聞 查看精彩圖片

修改后 的默認(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 )。

近期精彩回顧