1️⃣首先根据大佬的博客,我们可以很快定位到后处理发生的地方(整个函数的靠后处):
进入AddPostProcessingPasses
,然后根据笔记(1)(参考官方各种文档),可以知道FSR
作为UpScale
是位于Tone Mapping
之后的,所以我们直接略过中间复杂的过程,可以很快找到主放大Pass和副放大Pass:
2️⃣考虑主放大Pass,在设置完PassInputs
之后,核心代码就是下面这一行:
CustomUpscaler
的类型是ISpatialUpscaler
,通过官方注释,我们可以知道:ISpatialUpscaler
是自定义空间缩放算法的接口,意在通过ISceneViewExtension::BeginRenderViewFamily()
对FSceneViewFamily
进行设置。
这个暂且不谈,我们通过代码可以知道,CustomUpscaler
实际上存储在View.Family
中,而视图类中Family
的类型是FSceneViewFamily
(其实,可以直接参考注释了,但我当时还没发现)。这个ViewFamily.SpatialUpscaler
的设置应该是在FSceneRenderer
的构造函数中:
FSceneRenderer
的构造又是发生在FRendererModule::BeginRenderingViewFamily
中:
而FSceneViewFamily
一开始,我以为是在这个函数的调用处赋值的(毕竟是作为参数传进来),但通过一系列Check
(这些检查函数都需要保证其对应成员之前没有值)和注释(终于用到了),我们确定了其填充过程应该是这个:
但是我们跳转到这个函数,发现这个函数只是个虚函数,没有实际过程,而这个答案很明显在FSR的源码中。
3️⃣在跳转到FSR
的源码部分之前,我们需要解决一个疑惑——ViewFamily
的ViewExtensions
是在哪里填充的?
根据大佬的博客,我们可以知道有这样一个调用链:
我们看看UGameViewportClient::Draw
,然后我们会发现viewFamily
的构造,以及下面这样一行代码:
ViewFamily.ViewExtensions = GEngine->ViewExtensions->GatherActiveExtensions(FSceneViewExtensionContext(InViewport));
感觉没有必要继续挖了,有点累了,先这样吧。
4️⃣我们现在跳转到FSR
的源码部分。
1️⃣我们很快就能发现,FSR
代码中存在一个类FFSRViewExtension
,它继承了FSceneViewExtensionBase
,同时也实现了BeginRenderViewFamily
:
PS:因为要进行分析,所以就不截图了。此外,为了方便理解,编辑器部分的代码我都删掉了
void FFSRViewExtension::BeginRenderViewFamily(FSceneViewFamily& InViewFamily)
{
// the object(s) we assign here will get deleted by UE4 when the scene view tears down, so we need to instantiate a new one every frame.
// 我们在这里指定的对象将在场景视图关闭时被UE4删除,所以我们需要在每一帧实例化一个新的对象。
if (InViewFamily.GetFeatureLevel() >= ERHIFeatureLevel::SM5 && CVarEnableFSR.GetValueOnAnyThread() > 0)
{
TArray<TSharedPtr<FFSRData>> ViewData;
bool IsTemporalUpscalingRequested = false;
// 遍历Views,填充FSRData
for (int i = 0; i < InViewFamily.Views.Num(); i++)
{
const FSceneView* InView = InViewFamily.Views[i];
if (ensure(InView))
{
// if any view is using temporal upscaling, use the Combined upscaling mode.
// 如果任何视图使用temporal upscaling,则使用Combined upscaling mode
IsTemporalUpscalingRequested |= (InView->PrimaryScreenPercentageMethod == EPrimaryScreenPercentageMethod::TemporalUpscale);
// TSharedPtr will clean up this allocation
// 填充FSRData,准确说,是引擎提供的通用参数(视图相关的参数)
// 关于FSR算法所需要的特定参数(视图无关参数),应该是在其他地方填充好了
FFSRData* Data = new FFSRData();
Data->PostProcess_GrainIntensity = InView->FinalPostProcessSettings.GrainIntensity;
Data->PostProcess_GrainJitter = InView->FinalPostProcessSettings.GrainJitter;
Data->PostProcess_SceneFringeIntensity = InView->FinalPostProcessSettings.SceneFringeIntensity;
Data->PostProcess_ChromaticAberrationStartOffset = InView->FinalPostProcessSettings.ChromaticAberrationStartOffset;
ViewData.Add(TSharedPtr<FFSRData>(Data));
}
}
// 有视图使用temporal upscaling吗
if (!IsTemporalUpscalingRequested)
{
// FSR Upscale
InViewFamily.SetPrimarySpatialUpscalerInterface(new FFSRSpatialUpscaler(EFSRMode::UpscalingOnly, ViewData));
// 是否进行副放大Pass
if (!IsEASUTheLastPass())
{
InViewFamily.SetSecondarySpatialUpscalerInterface(new FFSRSpatialUpscaler(EFSRMode::PostProcessingOnly, ViewData));
}
}
else
{
// 混合模式
InViewFamily.SetSecondarySpatialUpscalerInterface(new FFSRSpatialUpscaler(EFSRMode::Combined, ViewData));
}
}
}
基本的分析都在上诉代码的注释中,我们首先需要留意的这个部分:
InViewFamily.GetFeatureLevel() >= ERHIFeatureLevel::SM5
别忘了我们的目标——让FSR
可以在手机端运行,而手机端应该是Opengl ES 3.x
,所以后续我们应该将这个判断修改一下!加一个四级标题备注下:
回到正题,终于找到正主了,继承了ISpatialUpscaler
的FFSRSpatialUpscaler
,忽略混合模式和副放大Pass,那么接下来的重点就是,进入分析FFSRSpatialUpscaler
!
1️⃣首先,承接上文,我们要进行分析的是FFSRSpatialUpscaler
的构造函数:
所以,实际上,只要不是None
,所有的EFSRMode
都会进行如上的7
个subPass
。
2️⃣然后,我们进入AddPasses
(世界线终于收束了):
#define EXECUTE_STEP(step) \
for (FFSRSubpass* Subpass : FSRSubpasses) \
{ \
Subpass->step(GraphBuilder, View, PassInputs); \
}
FScreenPassTexture FFSRSpatialUpscaler::AddPasses(FRDGBuilder& GraphBuilder, const FViewInfo& View, const FInputs& PassInputs) const
{
RDG_GPU_STAT_SCOPE(GraphBuilder, FidelityFXSuperResolutionPass);
check(PassInputs.SceneColor.IsValid());
// 获取View对应的FSRData(之前填充的)
TSharedPtr<FFSRData> Data = GetDataForView(View);
// 遍历subPass,让其内部对应指针,指向这个Data
// SetData是个虚函数,我随便打开了一个派生类,没有override,估计只有少数几个派生类需要override
for (FFSRSubpass* Subpass : FSRSubpasses)
{
Subpass->SetData(Data.Get());
}
// 如果数据没有初始化
// ParseEnvironment、CreateResources都是FSRsubPass类的虚函数
if (!Data->bInitialized)
{
// 解析环境,设置Data对应的成员变量:bUSE_FP16、bFORCE_VSPS、bRCASEnabled等。取决于派生类
// 遍历所有subPass
EXECUTE_STEP(ParseEnvironment);
// 创建Data中的一些资源变量,例如:UpscaleTexture、SharpenedTexture等。取决于派生类
// 遍历所有subPass
EXECUTE_STEP(CreateResources);
}
// 根据模式,决定不一样的运行方式。
if (Mode == EFSRMode::UpscalingOnly || Mode == EFSRMode::Combined)
{
// 遍历所有subPass
// 调用Upscale
EXECUTE_STEP(Upscale);
}
if (Mode == EFSRMode::PostProcessingOnly || Mode == EFSRMode::Combined)
{
EXECUTE_STEP(PostProcess);
}
// 获取输出结果
FScreenPassTexture FinalOutput = Data->FinalOutput;
// 返回最终结果,UE的右移?该复习下了C++了
return MoveTemp(FinalOutput);
}
主要代码分析见上诉注释,而EXECUTE_STEP(Upscale);
也就是循环调用各个subPass
的UpScale
虚函数。
目前先不分析FSR
本身算法的流程。让我们看看怎么在移动端开启它。
进入到AddMobilePostProcessingPasses
,其基本逻辑和之前分析的PC端延迟渲染差不多,我们直接定位到主放大Pass:(手机端没有次放大Pass的设置)
// Apply ScreenPercentage
if (PassSequence.IsEnabled(EPass::PrimaryUpscale))
{
ISpatialUpscaler::FInputs PassInputs;
PassSequence.AcceptOverrideIfLastPass(EPass::PrimaryUpscale, PassInputs.OverrideOutput);
PassInputs.Stage = EUpscaleStage::PrimaryToOutput;
PassInputs.SceneColor = SceneColor;
PassInputs.OverrideOutput.LoadAction = View.IsFirstInFamily() ? ERenderTargetLoadAction::EClear : ERenderTargetLoadAction::ELoad;
if (const ISpatialUpscaler* CustomUpscaler = View.Family->GetPrimarySpatialUpscalerInterface())
{
RDG_EVENT_SCOPE(
GraphBuilder,
"ThirdParty PrimaryUpscale %s %dx%d -> %dx%d",
CustomUpscaler->GetDebugName(),
SceneColor.ViewRect.Width(), SceneColor.ViewRect.Height(),
View.UnscaledViewRect.Width(), View.UnscaledViewRect.Height());
SceneColor = CustomUpscaler->AddPasses(GraphBuilder, View, PassInputs);
if (PassSequence.IsLastPass(EPass::PrimaryUpscale))
{
check(SceneColor == ViewFamilyOutput);
}
else
{
check(SceneColor.ViewRect.Size() == View.UnscaledViewRect.Size());
}
}
else
{
SceneColor = ISpatialUpscaler::AddDefaultUpscalePass(GraphBuilder, View, PassInputs, EUpscaleMethod::Bilinear, PaniniConfig);
}
}
上面的逻辑基本也和之前的PC端一致,所以目前只发现了一个修改点,我去试试改下会发生什么。
1️⃣首先,我们基于第一个修改点,做了如下简单修改:
然后,编译,打开编辑器,切换到ES3.1
,果然崩溃。报错如下:
检查后,目测应该是EASU subpass
的Computer shader
生成失败,然后修改了ShouldCompilePermutation
(让这个着色器可以进行编译),如下:
SceneColor
是AddPasses
的返回值(理论上是后处理结果),而ViewFamilyOutput
是在后处理起点处定义的RT
,然后用来构造PassSequence
:
bool AcceptOverrideIfLastPass(EPass Pass, FScreenPassRenderTarget& OutTargetToOverride, const TOptional<int32>& AfterPassCallbackIndex = TOptional<int32>())
{
bool bLastAfterPass = AfterPass[(int32)Pass].Num() == 0;
if (AfterPassCallbackIndex)
{
bLastAfterPass = AfterPassCallbackIndex.GetValue() == AfterPass[(int32)Pass].Num() - 1;
}
else
{
// Display debug information for a Pass unless it is an after pass.
AcceptPass(Pass);
}
// We need to override output only if this is the last pass and the last after pass.
// 我们需要覆盖输出,只有当这是最后一次Pass和最后一次after pass时。
if (IsLastPass(Pass) && bLastAfterPass)
{
OutTargetToOverride = OverrideOutput;
return true;
}
return false;
}
经过一些查证和本人的理解(可能很多错误):在后处理过程中,每次传递的都是SceneColor
,而其类型FScreenPassTexture
,这个根据注释就知道:它只作为后处理链中的数据载体,如果要想将最终结果显示在屏幕上,那么最好的方法就是——在最后一个后处理Pass,将Output
设置为RT
(也就是ViewFamilyOutput
)。
总结来说,报错的原因是两个:
Pass
FSR
流程中),出现了问题3️⃣第一个原因很简单就可以知道不对:
那么就去看看FSR
吧:(First
和Last
就不用看了)
依然没有用,仔细分析下流程,我们实际加入的SubPass
只有三个:HDR
、EASU
、RCAS
。然后仔细看看,就会发现HDR
没有走。所以嫌疑犯就只剩下了两个。这个时候反复使用check
中断大法:
check(1==0);
我们知道,目前引擎走的是FSR
,而不是Combined
,所以只会调用subPass
的UpScale
虚函数,而不会调用subPass
的Postprocess
虚函数,所以犯人只剩下了EASU
。
继续使用check
中断,我们发现走到是Computer Shader
分支,仔细检查代码,并没有什么特殊情况:
4️⃣返璞归真,过程是不可能有问题,那只能是Output
有问题,我们很快发现:
而使用check
,我们知道了bUseIntermediateRT
是true
,所以:
Output = FScreenPassRenderTarget(Data->UpscaleTexture.Texture, Data->UpscaleTexture.ViewRect, ERenderTargetLoadAction::ENoAction)
所以,破案了:七个subpass
实际只有EASU subpass
在发挥作用,但是这里却没有使用PassInputs.OverrideOutput
作为输出RT,而是一个临时RT!。自然而然,我们就不可能通过报错的那个check
。
如果EASU subpass
是一个中间Pass
,这样做自然没有问题,但是问题在于,这里它是独苗,看看这个bool
类型的赋值:
const bool bUseIntermediateRT = (Data->bRCASEnabled || Data->bChromaticAberrationPassEnabled) || !PassInputs.OverrideOutput.IsValid();
意思很简单:只有当后续的RCAS
或ChromaticAberration
存在时,又或者PassInputs.OverrideOutput
不存在时,我们才使用临时RT。而现在,很明显是前者的问题,通过check
,我们发现了bChromaticAberrationPassEnabled
是false
,而bRCASEnabled
是true
。问题来了,我们并不会走RCAS
!
5️⃣为什么会这样呢?AMD在UE4里面是不希望,或者说目前未考虑支持移动管线,所以它考虑bRCASEnabled
是很简单的:
Data->bRCASEnabled = GFSR_RCAS > 0;
这里逻辑实在很奇怪,我感觉自己的逻辑能力也解释不清,直接给出解决方法:在Only FSR
的情况下,直接设置bRCASEnabled
是false
。
以上都是年前处理的,年后似乎发现当时的修改实际没有生效?所以后续又改了一下?
哦哦哦!对了,上诉结果是不对的,这么差的效果怎么可能是FSR
,这就是UE4
自带的放大Pass
!为什么?我忘了在插件里面开启FSR
这个插件了!所以,看到这里的朋友们,别忘了!去插件设置里面,启用FSR
插件!
PS:修改的最后一个问题是:似乎没有考虑编辑器模式下的处理,所以在编辑器模式下,整个场景都是黑的。但没关系,游戏模式是正常的。你可以看到
FSR
的强大之处!这个问题搁置(不影响我使用麻,叉腰),我暂时没时间搞这个了
下一篇:4.26的适配。