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窗口会渲染异常, 同时疯狂报错. 所以解决方案就是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;
}1
2
3
4
5
6public override bool SupportsNativeRenderPass()
{
// 原判断条件
// return settings.Event <= RenderPassEvent.BeforeRenderingPostProcessing;
return true;
}
吐槽: 这种函数为什么要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
};
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
FRAMEBUFFER_INPUT_HALF(GBUFFER0);
FRAMEBUFFER_INPUT_HALF(GBUFFER1);
FRAMEBUFFER_INPUT_HALF(GBUFFER2);
FRAMEBUFFER_INPUT_FLOAT(GBUFFER3);
FRAMEBUFFER_INPUT_HALF(GBUFFER4);
TEXTURE2D_X_HALF(_GBuffer4);
//TEXTURE2D_X_HALF(_GBuffer5);
TEXTURE2D_X(_GBuffer5);
TEXTURE2D_X(_GBuffer6);
//...
half4 DeferredShading(Varyings input) : SV_Target
{
//...
float4 renderingLayers = LOAD_FRAMEBUFFER_INPUT(GBUFFER4, input.positionCS.xy);
float4 renderingLayers = LOAD_TEXTURE2D_X_LOD(MERGE_NAME(_, GBUFFER_LIGHT_LAYERS), input.positionCS.xy, 0);
uint meshRenderingLayers = uint(renderingLayers.r * 255.5);
uint meshRenderingLayers = DEFAULT_LIGHT_LAYERS;
//...
}
不过真机测试时, Redmi K30 Pro(865)是可以正常渲染的, 而Mi 8(845)就会出现花屏. 具体是不是因为845不支持大于四张的InputAttachment还有待验证与查阅资料.
吐槽: 前面那种小坑文档不说明也就算了, 这种也不说明? 全靠用户琢磨, 你以为你是UE?
大概就是这样, 希望可以帮到遇到同样问题的朋友.
原文链接: https://zhuanlan.zhihu.com/p/574540329