URP延迟渲染+Native Renderpass踩坑记录

过去两周多折腾URP Deferred + Native Renderpass, 踩了无数的坑, 翻遍国内国外的社区也找不到太多可供参考的资料. 于是在这里简述一下一些不可回避的问题以及其解决方案, 也算是为社区做点贡献.

在开始之前, 我想延迟渲染这种烂大街的玩意就没什么讲的必要了, 而Native Renderpass可能不少人没听过, 这东西底层调用的其实就是vulkan的renderpass/subpass API(metal也有类似的一套), 简单来说就是利用移动端TB(D)R的硬件架构, 相比于传统延迟管线在basepass结束后将gbuffer store回system memory, 之后再在lightpass中load回来这种带宽压力极大的方案, Native Renderpass可以在每个tile的basepass结束后将gbuffer保存在On-Chip Memory上, 以供接下来的lightpass直接使用, 直接优化掉了两个pass之间的store/load操作, 极大减缓了带宽压力, 这种形式的rt也被称之为memoryless.

简单科普到此为止, 详细内容可以看vulkan官方讲subpass的ppt(很好找)或者是其他知乎大佬的文章.

那么接下来就直接讲我踩到的几个坑以及解决方案. 因为是项目素材所以截图是肯定不能放的, 感兴趣可以自己试试去复现, 当然只是URP源码部分的代码我会贴出来.

开启Native RenderPass时激活SceneView窗口导致Native RenderPass失效

很诡异的问题, 不只是在Editor中会这样, 甚至还会影响打包的结果.
开始以为是Bug, 分析源码后发现其原因在于源码中对Native RenderPass的开启做了两次二重判定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// UniversalRenderer.cs
public override void Setup(ScriptableRenderContext context, ref RenderingData renderingData)
{
//...
if (cameraData.cameraType != CameraType.Game)
useRenderPassEnabled = false;
//...
}

// DeferredLights.cs
internal void SetupLights(ScriptableRenderContext context, ref RenderingData renderingData)
{
//...
CoreUtils.SetKeyword(cmd, ShaderKeywordStrings.RenderPassEnabled, this.UseRenderPass && renderingData.cameraData.cameraType == CameraType.Game);
//...
}

也就是说在Scene窗口激活时, 即使开启Native RenderPass(useRenderPassEnabled == true), 也是无效的.

虽然通过修改代码可以强行开启, 但Scene窗口会渲染异常, 同时疯狂报错. 所以解决方案就是framedebug/打包等等操作时找个窗口盖住scene窗口, 然后重新开关Native Rednerpass即可.

充分怀疑是SceneViewCamera存在Unity暂时解决不了的Bug或没有对vk renderpass做适配.

吐槽: 这么重要的问题文档一句不提???

开启Native RenderPass后自定义的RendererFeature渲染出错

图就不放了, 总之打包到移动端真机后哪个tile用到了自定义的RendererFeature哪个tile就马赛克, 完全炸了.

原因是ScriptableRenderer.cs里特别定义了变量用于控制RendererFeature的Native Renderpass的开闭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// Temporary variable to disable custom passes using render pass ( due to it potentially breaking projects with custom render features )
// To enable it - override SupportsNativeRenderPass method in the feature and return true
internal bool disableNativeRenderPassInFeatures = false;

protected void AddRenderPasses(ref RenderingData renderingData)
{
//...
// Add render passes from custom renderer features
for (int i = 0; i < rendererFeatures.Count; ++i)
{
if (!rendererFeatures[i].isActive)
{
continue;
}

if (!rendererFeatures[i].SupportsNativeRenderPass())
disableNativeRenderPassInFeatures = true;

rendererFeatures[i].AddRenderPasses(this, ref renderingData);
disableNativeRenderPassInFeatures = false;
}
//...
}

// ScriptableRendererFeature.AddRenderPasses()内部调用ScriptableRenderer.EnqueuePass():
public void EnqueuePass(ScriptableRenderPass pass)
{
m_ActiveRenderPassQueue.Add(pass);
if (disableNativeRenderPassInFeatures)
pass.useNativeRenderPass = false;
}

解决方案是在自定义的RendererFeature里重写SupportsNativeRenderPass()函数, 在明确知道符合原条件的情况下, 直接返回ture即可.

1
2
3
4
5
6
public override bool SupportsNativeRenderPass()
{
// 原判断条件
// return settings.Event <= RenderPassEvent.BeforeRenderingPostProcessing;
return true;
}

这里需要注意的是父类ScriptableRendererFeature里定义的SupportsNativeRenderPass()函数的访问级别是internal, 想要在自己的命名空间里重写该函数就要全改成public.

吐槽: 这种函数为什么要internal? 不考虑用户扩展?

开启Native RenderPass后移动Camera, Light Layer会出现错位/拖影

逆天大坑, 感兴趣的朋友可以自己试试开个默认场景然后改改light layer再往手机上打个包, 简单说就是在移动camera时会出现light layer不同步的拖影, 实际表现就像是light layer的屏幕空间位置刷新跟不上角色的刷新一样.

折腾了很久后终于排查到了问题所在:

因为Vulkan的一个renderpass会在所有subpass结束后再把要store回system memory的数据store, 也就是说LightPass里load进来的并不是同一帧里前一个subpass(BasePass)生成的light layer, 而是上一帧renderpass结束后store回system memory的light layer, 故而导致移动摄像机时会拖影.

解决方案: 在DeferredLights.cs的Setup()函数中修改以下两处定义为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//this.DeferredInputAttachments = new RenderTargetIdentifier[4]
//{
// this.GbufferAttachmentIdentifiers[0], this.GbufferAttachmentIdentifiers[1],
// this.GbufferAttachmentIdentifiers[2], this.GbufferAttachmentIdentifiers[4]
//};
this.DeferredInputAttachments = new RenderTargetIdentifier[5]
{
this.GbufferAttachmentIdentifiers[0], this.GbufferAttachmentIdentifiers[1],
this.GbufferAttachmentIdentifiers[2], this.GbufferAttachmentIdentifiers[4],
this.GbufferAttachmentIdentifiers[5]
};
//this.DeferredInputIsTransient = new bool[4]
//{
// true, true, true, false
//};
this.DeferredInputIsTransient = new bool[5]
{
true, true, true, false, true // DeferredInputIsTransient[3]也就是Depth as Color貌似也可以写成true
};

可以看到URP在源码里写死了InputAttachment最大数量为4, 通过DeferredInputAttachments数组指定其为gbuffer0/1/2/4, 并通过DeferredInputIsTransient数组指定对应的gbuffer是否为memoryless, 所以导致light layer只能从system memroy load上一帧的结果. 这恐怕也是因为Unity考虑到要兼容部分只支持四张InputAttachment的Vulkan设备而做的妥协.

C#端开启完毕后, 就可以在shader里用InputAttachment的方式从On-Chip Memory里读取当前帧在basepass生成的lightlayer了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// StencilDeferred.shader
#if _RENDER_PASS_ENABLED
#define GBUFFER0 0
#define GBUFFER1 1
#define GBUFFER2 2
#define GBUFFER3 3
#ifdef _LIGHT_LAYERS
#define GBUFFER4 4
#endif

FRAMEBUFFER_INPUT_HALF(GBUFFER0);
FRAMEBUFFER_INPUT_HALF(GBUFFER1);
FRAMEBUFFER_INPUT_HALF(GBUFFER2);
FRAMEBUFFER_INPUT_FLOAT(GBUFFER3);
#ifdef _LIGHT_LAYERS
FRAMEBUFFER_INPUT_HALF(GBUFFER4);
#endif
#else
#ifdef GBUFFER_OPTIONAL_SLOT_1
TEXTURE2D_X_HALF(_GBuffer4);
#endif
#endif

#if defined(GBUFFER_OPTIONAL_SLOT_2) && _RENDER_PASS_ENABLED
//TEXTURE2D_X_HALF(_GBuffer5);
#elif defined(GBUFFER_OPTIONAL_SLOT_2)
TEXTURE2D_X(_GBuffer5);
#endif
#ifdef GBUFFER_OPTIONAL_SLOT_3
TEXTURE2D_X(_GBuffer6);
#endif

//...

half4 DeferredShading(Varyings input) : SV_Target
{
//...
#ifdef _LIGHT_LAYERS
#ifdef _RENDER_PASS_ENABLED
float4 renderingLayers = LOAD_FRAMEBUFFER_INPUT(GBUFFER4, input.positionCS.xy);
#else
float4 renderingLayers = LOAD_TEXTURE2D_X_LOD(MERGE_NAME(_, GBUFFER_LIGHT_LAYERS), input.positionCS.xy, 0);
#endif
uint meshRenderingLayers = uint(renderingLayers.r * 255.5);
#else
uint meshRenderingLayers = DEFAULT_LIGHT_LAYERS;
#endif
//...
}

这样一来gbuffer5就成功被设置成了memoryless, 再次打包发现light layer错位/拖影的现象已经消失了.

不过真机测试时, Redmi K30 Pro(865)是可以正常渲染的, 而Mi 8(845)就会出现花屏. 具体是不是因为845不支持大于四张的InputAttachment还有待验证与查阅资料.

吐槽: 前面那种小坑文档不说明也就算了, 这种也不说明? 全靠用户琢磨, 你以为你是UE?

大概就是这样, 希望可以帮到遇到同样问题的朋友.

原文链接: https://zhuanlan.zhihu.com/p/574540329