序
随着以后越来越多的手游向“3A”聚拢,手机上的各种性能优化也在致力地为“3A”保驾护航,巴不得要把芯片上每一个晶体管的性能都开掘进去。然而,当一台“高分低能”的手机摆在你背后的时候,是不是总是有一种“欲哭无泪”的无力感——既要放弃高帧率又要保障画面质量。成年人从来不做选择题,在两个都要的状况下,降分辨率往往是起效最快的方法。
说到调整设施的分辨率,Screen.SetResolution这个办法大家必定是很相熟了,然而这种调整是全局的,是硬件级别的调整,无奈做到3D和UI渲染指标的离开调整。当然,随着SRP管线的推出,咱们曾经能够实现3D相机和UI相机分辨率的离开调整,并且UWA上已有相干文章的介绍了(见参考9)。
明天这篇文章要探讨的是,Unity和Unreal都提供的动静分辨率的计划,它能够动静缩放单个渲染指标,以缩小GPU上的工作量。
说到3D和UI的离开渲染,聪慧的小伙伴必定想到了一种计划:3D渲染到一张RT上,最初把3D的RT Blit到最终的RT上。那么这种计划跟Unity提出的动静分辨率计划有何不同的中央吗?还是说只是新瓶装旧酒?
接下来,我就跟大家一起摸索一下动静分辨率(以Unity为主)的原理以及它的利用场景。
传统的3D和UI拆散计划
如上图所示,基本原理是渲染场景的时候调整视口大小(Viewport),将渲染束缚到屏幕外Render Target的一部分,而后再把场景的Render Target上的内容Blit到最终的RT上。例如,渲染指标的大小可能为(1920,1080),但视口的原点可能为(0,0),大小为 (1280,720)。
这种实现形式可能会有如下几个问题:
- Blit的性能损耗,这个操作必定不能是实时的,个别也就是在游戏初始化后或者在进入某个场景前设置一次,是一个低频操作,无奈做到真正的“实时”调整。
可能受限于渲染管线
- 如果是默认渲染管线的话,最初这个Blit的操作机会就要选好,因为游戏中个别会有后处理阶段,咱们要利用好这个阶段顺便把Blit也做了。这个能够利用CommandBuffer向相机的不同渲染阶段插入视口批改和后处理操作。
- 如果是SRP渲染管线的话(Unity 2018当前的版本),咱们就能有本人解决Blit的机会了,当然这个操作也不能是个高频操作。
应用流程
参考Unity官网文档,咱们先来看一下动静分辨率的应用流程。
首先咱们要确认一点:动静分辨率启用的前提是GPU Bound了。所以要通过实时获取每帧GPU的运行工夫来决定:
- 是否是GPU压力过大导致游戏掉帧
- 渲染指标的缩放系数
再依据缩放系数对渲染指标进行动静缩放。在这个过程中须要保障批改渲染指标分辨率的时候不重新分配GPU显存,否则就跟Screen.SetResolution一样了(会导致画面闪动)。
- 在须要动静缩放的相机上勾选,如图所示:
- 在PlayerSettings中勾选上“Enable Frame Timing Stats”:
- 通过两个接口FrameTimingManager.CaptureFrameTimings()和FrameTimingManager.GetLatestTimings获取CPUTime和GPUTime后自行判断缩放系数
- 最初调用ScalableBufferManager.ResizeBuffers(m_widthScale, m_heightScale)设置缩放
平台反对
Unity官网文档上是这么写的:
可能跟了解程度有关系,看到以上的阐明,我就犯迷糊了:OpenGLES不反对动静分辨率,内置渲染管线、URP等兼容,那么如果是URP下的OpenGLES平台呢?反对还是不反对?
不论如何,先把纳闷放一边,咱们来探索一下动静分辨率的实现原理。
原理探索
咱们顺着官网文档上的应用流程,摸入Unity源码外部,看看为什么对OpenGLES如此一视同仁。因为波及到源码局部,这里就间接说论断了。
- 缩放RT是跟平台相干的,OpenGLES无奈创立缩放RT,起因咱们前面再讲
- 动静分辨率的原理为Vulkan的内存混叠(Memory Aliasing)性能
Memory Aliasing
Memory Aliasing能够翻译成内存混叠或内存别名,参考[1]是Vulkan针对此概念的阐明。
古代图形 API(如DirectX 12或Vulkan)能够让用户定义内存地位,将调配的GPU资源放入手动创立的堆中。它容许咱们创立纹理和缓冲区,它们的内存局部甚至能够齐全重叠。这也是为什么OpenGLES不反对动静分辨率的起因,因为OpenGLES没有凋谢更底层的API让咱们能够实现更高效的内存治理。
以游戏中典型的一帧为例:光栅化一些几何体,执行着色,而后运行一堆后处理。这里的每个阶段的输入都将写入纹理或缓冲区,稍后在一帧中被其余阶段应用。然而,某个阶段产生的资源可能只被多数其余阶段应用,比方在后处理中:Bloom产生的输入,只会被下一阶段的Tone mapping(色调映射)应用,并且在帧中的其余任何中央都不须要。咱们能够看到,资源的无效生命周期可能很短,但很可能是事后调配的,并且在整个帧中都占用了它的内存。
解决内存频繁调配开释的办法就是对象池,Unity的RenderTexture.GetTemporary就是在外部保护了一个RenderTexture的对象池。然而这种办法只实用于后处理阶段,因为不同格局、大小的资源不能复用,后处理通常是全屏的Pass,读取、写入的Texture通常都有雷同的属性,一些简略的后处理只须要两个RT重复交替应用就能实现(这个我会在稍后的URP章节中重点解读一下) 。
对象池实质上是一种更下层的Memory Aliasing,开发者不须要关注内存治理;但古代图形API(DX12和Vulkan)提供了内存治理的接口,能够实现底层的Memory Aliasing。Memory Aliasing指的是不同变量指向同一地址,即在同一片内存区域中同时寄存多个资源,如果有很多大型资源在工夫上不会重叠,就能够在雷同的内存调配这些资源。相比对象池,Memory Aliasing能够进一步升高内存占用,因为在底层都是一堆字节,所以就不须要思考资源的类型、格局、大小等。具体的示意如下图所示:
小结
从以上的剖析咱们大略理解到了Unity实现动静分辨率的原理:利用Vulkan提供的内存治理接口,实现底层对内存高效地复用。这样咱们在游戏中就能够高效实时地调整分辨率,根本没有性能损耗。
URP实现
思考到URP的前身LWRP还有项目组在用,上面先简略看一下LWRP。
LWRP
简略点说就是通过从新创立相机的渲染指标来实现的。Setup时会先进入函数RequiresIntermediateColorTexture判断是否要创立新的RT,外面就有个变量isScaledRender,如果须要缩放,则进入创立RT的Pass:
m_CreateLightweightRenderTexturesPasspublic void Setup(ScriptableRenderer renderer, ref RenderingData renderingData){ ... bool requiresRenderToTexture = ScriptableRenderer.RequiresIntermediateColorTexture(ref renderingData.cameraData, baseDescriptor); RenderTargetHandle colorHandle = RenderTargetHandle.CameraTarget; RenderTargetHandle depthHandle = RenderTargetHandle.CameraTarget; if (requiresRenderToTexture) { colorHandle = ColorAttachment; depthHandle = DepthAttachment; var sampleCount = (SampleCount)renderingData.cameraData.msaaSamples; m_CreateLightweightRenderTexturesPass.Setup(baseDescriptor, colorHandle, depthHandle, sampleCount); renderer.EnqueuePass(m_CreateLightweightRenderTexturesPass); } ...}public static bool RequiresIntermediateColorTexture(ref CameraData cameraData, RenderTextureDescriptor baseDescriptor){ if (cameraData.isOffscreenRender) return false; bool isScaledRender = !Mathf.Approximately(cameraData.renderScale, 1.0f); bool isTargetTexture2DArray = baseDescriptor.dimension == TextureDimension.Tex2DArray; bool noAutoResolveMsaa = cameraData.msaaSamples > 1 && !SystemInfo.supportsMultisampleAutoResolve; return noAutoResolveMsaa || cameraData.isSceneViewCamera || isScaledRender || cameraData.isHdrEnabled || cameraData.postProcessEnabled || cameraData.requiresOpaqueTexture || isTargetTexture2DArray || !cameraData.isDefaultViewport;}
URP
从Unity 2019.3.0a这个版本开始,LWRP开始正式降级为URP。URP次要分为两个文件夹:一个是独自提取进去跟HDRP共用的根底外围库core,另一个就是URP本人用的universal。
翻看了URP各个版本的代码,直到Core RP库10.2版本(对应Unity版本为2020.2.0b)开始,Unity才开始器重(提供)Render Target(渲染指标)的治理性能。
从上一章节的“原理探索”中,咱们晓得渲染指标治理是任何渲染管线的重要组成部分;咱们也晓得RenderTexture只有在新渲染纹理应用完全相同的属性和分辨率时能力重用内存。
为了解决渲染纹理内存调配的这些问题,Unity的SRP(URP&HDRP)引入了RTHandle零碎。该零碎是RenderTexture之上的一个形象层,可较好地治理渲染纹理,具体介绍能够看参考8,这里我就简略介绍一下。
如上截图中枚举所示,SRP实现了“硬件”和“软件”两种动静分辨率,“硬件动静分辨率“就是利用内存混叠硬实现的,而”软件动静分辨率“就是缩放RT适应以后视口的软实现。当硬件动静分辨率不反对以后平台时,RTHandle零碎会主动切换为软件动静分辨率。不仅如此,最新的URP版本还基于RTHandle实现了双缓冲,感兴趣的能够去URP源码查看RenderTargetBufferSystem。
利用
一路下来,咱们对“动静分辨率”也有了一个比拟粗浅的意识了,当说到“动静分辨率”时,咱们说的就是真正的硬件层面实现的动静分辨率,即:可能充分利用古代图形API的Memory Aliasing,为把FPS维持在肯定的程度,当产生GPU引起的掉帧时,可能在不重新分配GPU显存(利用图形API的Memory Aliasing)的状况下动静调整渲染指标分辨率。
然而,思考到设施的兼容性,咱们大部分游戏反对的平台都只能是OpenGLES而不是Vulkan,因而很遗憾,动静分辨率派不上用场了。退而求其次,针对不同的渲染管线,上面简略阐明一下咱们可能采纳的计划:
- 默认渲染管线——Unity 2017(含)以前的版本
- 能够应用本文“传统的3D和UI拆散计划”中介绍的计划,利用CommandBuffer在适合的机会对视口进行动静调整,但不能高频应用。
- LWRP——Unity 2018~Unity 2019.3.0a
- URP——Unity 2019.3.0a12+~Unity 2020.2.0b8+
LWRP作为URP的前身,有好多功能还在欠缺中,曾经能够比拟好地实现3D和UI的离开渲染,相比默认渲染管线灵活性更好了。但还是没有提供比拟好的RT治理,须要本人参考URP来定制一套高效的RT管理系统。
- URP——Unity 2020.2.0b12+
如上所述,直到SRP的Core RP库10.2版本开始,Unity才提供了一套比较完善的RT管理系统,大家能够酌情参考应用。
参考
[1] https://www.khronos.org/regis...
[2] 内存混叠的一种实现
[3] https://developer.nvidia.com/...
[4] https://docs.unrealengine.com...
[5] https://www.intel.com/content...
[6] https://docs.unity3d.com/Manu...
[7] https://github.com/Unity-Tech...
[8] https://docs.unity3d.com/Pack...
[9] 如何只降3D相机不降UI相机的分辨率
这是侑虎科技第1198篇文章,感激作者吕强供稿。欢送转发分享,未经作者受权请勿转载。如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)
作者在UWA学堂上线的《五天实现PBR保姆级教程》课程限时优惠中~
再次感激吕强的分享,如果您有任何独到的见解或者发现也欢送分割咱们,一起探讨。(QQ群:793972859)