
动机
在使用unity的时候,隔壁项目组做了一个优化,就是用不同的分辨率分辨渲染UI和场景,由于UI对分辨率的敏感性要更高。当然了,这种优化是非常普遍的,Unreal默认的行为就是这种,后面会介绍它的实现方案。我在我们自己的项目中也去做了这个优化,在实现的过程中,当时我在玩《马里奥奥德赛》,我能明显感觉到分辨率在运行时在动态改变。我在网上也查了一下,看到了这篇文章,也验证了我的感觉。随即就产生了,既然场景和UI已经分开了,那么更进一步,让场景能够在GPU瓶颈的时候动态改变来获得稳定的帧数。 当时我们使用的unity版本还比较老,是我自己实现的,后来unity自带了动态分辨率,但只对xbox开放,unreal现在也有实现,但只支持某些平台,现在的做法只需要改一些引擎的代码来支持我们需要的平台。由于现在我使用的是unreal了,所以,实现的介绍以unreal的为例。
动态分辨率的基本原理
基本原理很简单,就是在渲染场景的时候,根据GPU的使用情况,调整Viewport的大小,然后再把场景的rendertarget上的内容blit到最终的rt上。

当然,过程中一般是sub-sampling,你也可以让RT大于backbuffer的大小,这样可以super-sampling。
Query
在实现动态分辨率的中,很总要的一个点是获取GPU的运行时间,各个图形API都提供了叫Query的工具,用来获得GPU的时间。以vulkan为例,它以commands为单位计时,将结果保存到一个query pool里面,然后通过接口可以获取到结果,结果是一个timestamp,将结果相减可以得到时间。
Unreal的实现
在介绍Unreal的实现之前,先简单介绍一下unreal对于渲染分辨率viewport相关的一些东西。对于每一个scene render都有一个view的数组,也就是这个场景可能被渲染了多少次,在view中有个viewrect,就是存着viewport的信息,在每次draw的时候,就会使用这个数据设置viewport。动态分辨率就是,如何去设置这个viewport的策略之一。这是第一步,后面还需要把绘制的内容画到最终的RT上,过程就是上面提到的那样,把绘制的RT中viewport大小的像素采样拷贝到最终的RT上。 对于什么时候拷贝,Unreal有几个策略。之前提到的一个策略,就是把UI和场景的分辨率分开,unreal叫spatial upscale,过程如下图:

拷贝(后面所说的拷贝都包含采样过程)发生在UI渲染之前。还有一个策略叫 Temporal Upsample,拷贝的时机是发生在做TAA之前,或者说是在TAA发生的时候同时做了upscale和TAA,这样是为了提升TAA的质量,当然也增加了TAA及之后后处理的消耗。

在此基础上unreal更进一步,组合上面的两个让UI在更高的分辨率渲染,unreal叫secondary spatial upscale。

介绍这些的是为了说明,unreal在viewport操作上已经封装的很好,被广泛使用,而且动态分辨率是在这个基础上对分辨率的更改。 在以上的方案里面,动态分辨率是作用在view geometry上的,也就是在第一阶段的分辨率下作用的。在第一阶段的分辨率控制里,也就是scene的渲染中,有ISceneViewFamilyScreenPercentage
的接口负责对分辨率的计算,动态分辨率是其中的一个实现,包括有FLegacyScreenPercentageDriver
,FDefaultDynamicResolutionDriver
,ISceneViewFamilyScreenPercentage
(Oculus)。默认情况下也有FLegacyScreenPercentageDriver
来处理viewport的大小,主要处理可能后处理需要的对分辨率的修改。在渲染开始的时候,render函数里面会调用PrepareViewRectsForRendering
方法来设置viewport,方法里面就会调用ISceneViewFamilyScreenPercentage
的ComputePrimaryResolutionFractions_RenderThread
方法来获得我们设置的viewport的大小。动态分辨率是在渲染之前计算好的,ComputePrimaryResolutionFractions_RenderThread
只是取出来。 计算过程是这样的,首先需要N帧的GPU时间和CPU的game和render线程的时间,需要CPU时间是为了排除有可能是CPU的瓶颈导致了帧率下降,这个时候即便降低分辨率,对整体帧率也不会有影响。在排除CPU的瓶颈的可能下,对历史的记录每帧计算,unreal是假定GPU的时间正比于分辨率缩放的平方,
SuggestedResolutionFraction = (
FMath::Sqrt(TargetedGPUBusyTimeMs / FrameEntry.TotalFrameGPUBusyTimeMs)
* FrameEntry.ResolutionFraction);
简单就是这样计算的,然后对计算后的数值乘上权重,权重是随着计算帧离当前帧的距离,等比递减,然后再加权平均一下,得到最终的分辨率。 Unreal将策略封装在了一个FDynamicResolutionHeuristicProxy中,我们可以自己重写这个类来设计属于自己的动态分辨率的策略。我在使用过程中,用unreal自带的策略结果已经可以了,当然随着项目的需求改变,或许就不能满足了。 最近看到有同学说viewport是rendertarget,怕是对viewport产生了很大的误会,viewport只是需要跟一个backbuffer rendertarget做绑定罢了。
Unreal代码索引: 设置分辨率调整接口SetScreenPercentageInterface
(GameViewportClient.cpp->UGameViewportClient::Draw) 设置视口大小(SceneRendering.cpp->FSceneRenderer::PrepareViewRectsForRendering) 视口大小计算(DynamicResolution.cpp->FDynamicResolutionHeuristicProxy::RefreshCurentFrameResolutionFraction_RenderThread)
引用:
https://docs.unrealengine.com/en-US/Engine/Rendering/DynamicResolution/index.html
https://docs.unrealengine.com/en-US/Engine/Rendering/ScreenPercentage/index.html#temporalanti-aliasingupsample
https://nintendoeverything.com/super-mario-odyssey-full-tech-analysis/
https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/chap17.html