游戏中动态分辨率从原理到应用

随着当前越来越多的手游向“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)。

这种实现方式可能会有如下几个问题:

  1. Blit的性能损耗,这个操作肯定不能是实时的,一般也就是在游戏初始化后或者在进入某个场景前设置一次,是一个低频操作,无法做到真正的“实时”调整。

  2. 可能受限于渲染管线

  • 如果是默认渲染管线的话,最后这个Blit的操作时机就要选好,因为游戏中一般会有后处理阶段,我们要利用好这个阶段顺便把Blit也做了。这个可以利用CommandBuffer向相机的不同渲染阶段插入视口修改和后处理操作。
  • 如果是SRP渲染管线的话(Unity 2018以后的版本),我们就能有自己处理Blit的时机了,当然这个操作也不能是个高频操作。

使用流程

参考Unity官方文档,我们先来看一下动态分辨率的使用流程。

首先我们要确认一点:动态分辨率启用的前提是GPU Bound了。所以要通过实时获取每帧GPU的运行时间来决定:

  • 是否是GPU压力过大导致游戏掉帧
  • 渲染目标的缩放系数

再根据缩放系数对渲染目标进行动态缩放。在这个过程中需要保证修改渲染目标分辨率的时候不重新分配GPU显存,否则就跟Screen.SetResolution一样了(会导致画面闪烁)。

1. 在需要动态缩放的相机上勾选,如图所示:

2. 在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_CreateLightweightRenderTexturesPass

public 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] Vulkan® 1.0.225 - A Specification

[2] 内存混叠的一种实现

[3] Vulkan Memory Management

[4] https://docs.unrealengine.com/4.26/zh-CN/RenderingAndGraphics/DynamicResolution/

[5] Dynamic Resolution Rendering Article

[6] Unity - Manual: Dynamic resolution

[7] GitHub - Unity-Technologies/DynamicResolutionSample: A drop-in dynamic resolution script

[8] SRP Core | Core RP Library | 10.1.0

[9] 如何只降3D相机不降UI相机的分辨率


这是侑虎科技第1198篇文章,感谢作者吕强供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者还在UWA学堂上线了《五天实现PBR保姆级教程》,喜欢的同学可点击阅读。

再次感谢吕强的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
### 回答1: Unity双屏飞屏是指在一个Unity应用程序,将两个显示器连接成为一个连续的屏幕,并且应用程序的内容可以无缝地跨越这两个屏幕显示。 实现Unity双屏飞屏的原理如下: 1. 获取屏幕尺寸:首先,Unity会通过调用操作系统提供的API获取主显示器和辅助显示器的屏幕尺寸以及相对位置关系。 2. 定位摄像机:Unity会根据显示器的位置和分辨率来确定摄像机的位置和视野范围,以确保摄像机可以正确地渲染跨越多个显示器的场景。 3. 渲染分割:Unity将场景分割成多个视图,每个视图对应一个显示器。每个视图都有自己的摄像机和渲染目标,用于渲染特定显示器的图像。 4. 同步画面:为了实现屏幕画面的同步,Unity会使用多个摄像机同时渲染场景,并将各个视图的渲染结果进行合并,最终生成一个连续的图像。 5. 屏幕适配:为了在双屏飞屏保持良好的用户体验,Unity会根据显示器的分辨率和纵横比,自动调整场景的布局和UI元素的位置,以适应不同的屏幕设置。 6. 全局坐标映射:Unity使用一个全局坐标映射系统,将场景的物体的位置和尺寸进行映射,以确保它们正确地显示在双屏飞屏。 总结起来,Unity双屏飞屏通过获取屏幕尺寸、定位摄像机、渲染分割、同步画面、屏幕适配和全局坐标映射等步骤来实现。这样,用户可以通过Unity应用程序,在连接的两个显示器上获得一个无缝连续的屏幕体验。 ### 回答2: Unity双屏飞屏实现原理是通过将Unity游戏窗口在两个显示屏上进行扩展显示,从而实现游戏画面的双屏显示。具体实现步骤如下: 首先,需要在Unity的编辑器设置游戏窗口的分辨率为两个显示屏的总分辨率。这样Unity会自动将游戏画面进行扩展,使其能够同时在两个屏幕上显示。 其次,需要在游戏脚本进行相应的配置,以适配双屏显示。可以使用Unity的Screen类来获取显示屏的分辨率和位置等信息。可以通过获取两个屏幕的宽度之和和高度的最大值来设置游戏画面的分辨率,以确保画面能够充满两个屏幕并且不会变形。 然后,需要调整相机的视口(Viewport)来适应双屏显示。可以通过设置相机的视口大小和位置来实现画面在两个屏幕上的显示。可以使用相机的ViewportRect属性来设置视口,其视口的x和y参数代表相对于屏幕左下角的百分比,width和height参数代表相对于屏幕宽度和高度的百分比。 最后,可以在游戏脚本根据需要自定义双屏的显示效果。可以在渲染游戏画面之前对相机的投影矩阵进行修改,以实现不同的透视效果或者投影方式。 总结来说,Unity双屏飞屏实现原理主要包括设置游戏窗口分辨率,配置游戏脚本适配双屏显示,调整相机视口来适应双屏,以及自定义双屏的显示效果。通过这些步骤,可以实现Unity游戏画面在两个显示屏上的同时显示。 ### 回答3: Unity是一款流行的游戏引擎,可以方便地实现双屏飞屏效果。在Unity实现双屏飞屏的原理主要涉及到以下几个方面。 首先,Unity提供了多屏幕管理的功能,可以通过代码控制多个屏幕的布局和显示效果。通过调用Unity的屏幕管理接口,可以获取当前系统所有屏幕的信息,包括分辨率、位置等。通过获取到的屏幕信息,可以计算出所需的显示区域和布局方式。 其次,双屏飞屏效果的实现需要将屏幕空间映射到世界空间。通过使用Unity提供的相机组件,可以将屏幕空间的像素坐标转换为世界空间的坐标。通过设置相机的位置、旋转和投影方式,可以决定相机在世界空间的位置和视野范围。 另外,为了实现双屏飞屏的效果,还需要对游戏物体进行适配和处理。可以通过设置游戏物体的位置、尺寸和层级关系,使其在不同屏幕上按照预期的方式进行展示。同时,还可以通过使用Unity提供的层级管理和渲染队列等功能,对游戏物体的渲染进行控制,以实现双屏飞屏效果。 最后,Unity还提供了丰富的工具和插件,可以方便地进行双屏飞屏效果的调试和优化。通过使用调试工具和性能分析工具,可以查看双屏飞屏效果的实时表现和性能状况,并进行相应的调整和优化。 综上所述,Unity实现双屏飞屏的原理主要包括多屏幕管理、屏幕空间到世界空间的转换、游戏物体适配和处理,以及调试和优化等方面。通过综合应用这些功能和工具,可以方便地实现双屏飞屏效果,提升游戏的展示效果和用户体验。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值