【USparkle專欄】如果你深懷絕技,愛“搞點研究”,樂于分享也博采眾長,我們期待你的加入,讓智慧的火花碰撞交織,讓知識的傳遞生生不息!
這是侑虎科技第1779篇文章,感謝作者七塊君供稿。歡迎轉(zhuǎn)發(fā)分享,未經(jīng)作者授權(quán)請勿轉(zhuǎn)載。如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群:793972859)
作者主頁:
https://www.zhihu.com/people/Na-Ka-4921
一、前言
自Unity支持Linear色彩空間以來,Unity的3D渲染的光影層次變得更加準確細膩,但是Linear色彩空間下UI的渲染卻因此變得糟糕。絕大多數(shù)的UI資產(chǎn)都是在sRGB色彩空間里制作完成的,UI圖片的格式也都是基于sRGB,所以Linear色彩空間下,基于sRGB的UI圖片的Alpha得不到正確的混合,使得UI圖片的不透明度得不到正確的呈現(xiàn)。
為了匹配Linear色彩空間下的Alpha混合,有些團隊限制使用半透明資產(chǎn);有些是在Ps里盲調(diào),再到Unity里做驗證,直到看起舒服為止;或是在Ps里改變圖片色彩空間。這些做法無疑都是限制了UI制作流程。
現(xiàn)在的URP基于可編程渲染管線,支持自定義管線。要想徹底解決UI渲染問題,可以在Unity管線層面,做出合理的管線設(shè)計,維持UI設(shè)計師正常的sRGB資產(chǎn)制作流程。
二、管線效果對比
左邊: Photoshop效果 | 中間: 自定義UI管線效果 | 右邊: Unity URP默認效果
三、管線的設(shè)計思路
1. 在原有Linear色彩空間的Buffer里渲染3D圖形;
2. 將渲染完成的3D圖像轉(zhuǎn)移至Gamma色彩空間的UI Buffer中;
3. 在Gamma色彩空間的UI Buffer中繼續(xù)渲染UI圖片;
4. 將最終的渲染結(jié)果轉(zhuǎn)回到Linear,并最終輸出。
因為UI圖片的Alpha Blend是在Gamma空間下完成的,所以不存在錯誤的混合結(jié)果,兼容了Linear色彩空間的3D渲染和Gamma色彩空間下的UI渲染。
四、管線的具體實現(xiàn)
管線流程
思路有了,那么再結(jié)合URP現(xiàn)有的流程,詳細方案如下:
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里做色彩空間轉(zhuǎn)換,幾乎不會產(chǎn)生多少額外的性能開銷。只有在不使用Post-Processing時,才需要在3D渲染完成時額外補一個Pass做色彩空間轉(zhuǎn)換。
另外,Uber Post Process和Final Blit都會用到"Hidden/Universal Render Pipeline/Blit"這個Shader著色, 因此可以在Blit Shader的片元著色器里加入色彩空間轉(zhuǎn)換的函數(shù)和全局的Keyword宏,以方便在管線中利用Command Buffer設(shè)置keyword進行色彩空間轉(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默認渲染3D圖形的Buffer格式是RGBA111110Float:
如果在這個格式的Buffer里直接利用shader轉(zhuǎn)換色彩空間(LinearToSRGB)會造成3D圖像嚴重的色深精度丟失。所以需要一個和sRGB色彩信息契合的格式來儲存Shader轉(zhuǎn)換后色彩空間后的圖像信息,所以我選擇了RGBA32UNorm作為后續(xù)UI Buffer的格式,使用不同格式轉(zhuǎn)換色彩后的色深對比如下:
可以看出來,使用RGBA32UNorm能更好地儲存轉(zhuǎn)換后的色深精度。
轉(zhuǎn)換Buffer的方法,我這里未在ForwardRenderer里為UI重新聲明創(chuàng)建RT,以及根據(jù)是否是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)里,依據(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后面會講到。
UI的分辨率
因為在3D物體渲染完畢時切換了Buffer,在這里有一次全面重置Buffer尺寸的機會,我們可以修改接下來UI Buffer的分辨率,以達到即使降低3D的渲染質(zhì)量,也依然能保證UI以滿屏幕分辨率渲染。
在前面ForwardRenderer的CreateCameraRenderTarget()方法里創(chuàng)建UI RT時指定新的寬高尺寸:
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默認情況是UI在DrawTransparentObjects Pass里繪制,Game視圖因為有獨立的UI相機,所以問題不大,但是Scene視圖里只有一個相機,借助Render Doc可以看出UI和一般的半透明物體是混在一起的,想在UI和半透明物體之間插入自定義Pass是不可能的。理想狀態(tài)是:讓UI擁有屬于自己的Pass,方便后期維護管理。
在Forward Renderer中單獨聲明了一個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")來作為這個Pass的渲染條件。使用"UI" layer的Transparent序列物體都會進入這個Pass。
注:不能忘了重新配置Forward Renderer Data, 要把"UI" Layer Mask在Transparent Layer Mask中去掉,否則UI會被DrawTransparentObjects和UGUI這兩個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用一個全局變量"_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相機一切照舊,規(guī)避掉因為UI校色而導(dǎo)致非UI相機渲染的UI圖片(*例如:世界空間下的UI)顏色不正確。
注:即UI相機中UI的顏色和不透明度都是正確的,非UI相機中的UI顏色正確但不透明度未被矯正。所以暫時不支持非UI相機的不透明度矯正,但也不影響正常使用。
重置UI組件默認Shader
UI組件,比如Image,在不使用自定義材質(zhì)時,會默認使用Shader "UI/Defaut",而這個Shader是內(nèi)置不可編輯的,對于我們使用了自定的UI Shader和后期對Shader框架進行擴展就很不方便,我們需要UI組件默認使用我們自己寫的Shader。
UI 組件 默認著色器
好在UI組件的源碼是可以編輯的,順著Image組件的源碼,可以看到Image類繼承了MaskableGraphic類,MaskableGraphic類又繼承了Graphic類,這個就是UI組件的根源了。可以看到Graphic類里有一段設(shè)置默認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
文末,再次感謝 七塊君 的分享, 作者主頁: https://www.zhihu.com/people/Na-Ka-4921, 如果您有任何獨到的見解或者發(fā)現(xiàn)也歡迎聯(lián)系我們,一起探討。(QQ群: 793972859 )。
近期精彩回顧
特別聲明:以上內(nèi)容(如有圖片或視頻亦包括在內(nèi))為自媒體平臺“網(wǎng)易號”用戶上傳并發(fā)布,本平臺僅提供信息存儲服務(wù)。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.