找回密码
 立即注册
查看: 533|回复: 0

Mobile GpuCulling for Unreal

[复制链接]
发表于 2021-2-27 19:03 | 显示全部楼层 |阅读模式
简述

传统渲染管线在渲染时的最小剔除粒度是Object,当面对巨量植被或物件时通常使用的解决方案是将物体进行分组,然后CPU剔除再更新数据传递给GPU最后使用GPU Instance渲染。但由于每帧CPU和GPU频繁传递大量数据,这会造成大量的性能丢失。
Unreal中通过Hierarchical Instance减缓了这个问题(这里不详细展开其算法)其核心思想如为离线Cache分组数据,并在此基础上分层,进行视锥剔除、距离剔除,在一定范围内使用硬件遮挡查询进行遮挡剔除,最后根据屏幕占比计算LOD渲染物体。但这种方案也存在缺陷,由于不会每帧更新数据,当连续的Instance有一个被剔除时,必须再发起一个DrawCall进行渲染,如此带来的矛盾是:为了剔除更多的图元我们不得不将组分得更细,但分得越细带来得DrawCall也就越多,并且使用CPU渲染做剔除即使做多线程和异步在面对巨量Instance前也是无力的。
而随着硬件发展如今移动设备使用CS+IndirectDraw也是水到渠成的事了---将大量计算移到GPU并且做到更细粒度的剔除,并且支持Bound无回读的遮挡剔除。最终落地后测试在OpenGLES3.1, Metal2.0,Vulkan1.0以上均完美支持。
开发整个System主要资料和大量优化来源于天刀[1],不过天刀的Driven系统主要应用于地形,由于移动设备对于MultiDrawIndirect的支持不好植被和物件并没有动态更新。
CPU Build Cluster数据

构建传递给GPU的各个需要GPU Culling的Cluster数据,至于详细的划分粒度,数据结构,各种动态更新与Straming的策略要视具体场景和项目而定。例如对于植被使用Kd-Tree划分数据,但数量较少的物件每个Cluster直接塞入一个Instance就能达到很好的效果。
Compute Shader Build HIZ

在尝试了最传统的PS逐级构建Mip,和CS使用TGSM写出Texture等方案,最后发现最优方案是使用CS加上TGSM把Depth数据写出到Buffer中,整个过程如下图:
由于Mali存在Tex Buffer大小的限制,所以使用StructuredBuffer。之前还有一篇回读遮挡结果的笔记可以对比查看[2]。
Frustum Culling And Occlusion Culling

有了数据之后剔除就是水到渠成的事情了,FrustumCulling按照常规方式即可,而在Occlusion Culling方面,计算投影RectSize对应的Level然后采样即可。并且由于保守带来的优势,我们不需要更多的采样,经测试Sample Hiz时采样Bound投影在屏幕空间的Rect以及中心点就能得到不错的效果。下面是UE计算BoundLevel的代码片段,可在HZBOcclusion.usf中找到。
float3 BoundsMin = BoundsCenter.xyz - BoundsExtent.xyz;
float3 BoundsMax = BoundsCenter.xyz + BoundsExtent.xyz;
float3 Bounds[2] = { BoundsMin, BoundsMax };
       
// Screen rect from bounds
float3 RectMin = float3( 1, 1, 1 );
float3 RectMax = float3( -1, -1, -1 );
UNROLL for( int i = 0; i < 8; i++ )
{
        float3 PointSrc;
        PointSrc.x = Bounds[ (i >> 0) & 1 ].x;
        PointSrc.y = Bounds[ (i >> 1) & 1 ].y;
        PointSrc.z = Bounds[ (i >> 2) & 1 ].z;

        float4 PointClip = mul( float4( PointSrc, 1 ), View.TranslatedWorldToClip );
        float3 PointScreen = PointClip.xyz / PointClip.w;

        RectMin = min( RectMin, PointScreen );
        RectMax = max( RectMax, PointScreen );
}
// FIXME assumes DX
float4 Rect = saturate( float4( RectMin.xy, RectMax.xy ) * float2( 0.5, -0.5 ).xyxy + 0.5 ).xwzy;
float4 RectPixels = Rect * HZBSize.xyxy;
float2 RectSize = ( RectPixels.zw - RectPixels.xy ) * 0.5;        // 0.5 for 4x4
float Level = max(ceil( log2( max( RectSize.x, RectSize.y ) ) ), HZBUvFactor.z);

// Check if we can drop one level lower
float LevelLower = max( Level - 1, 0 );
float4 LowerRect = RectPixels * exp2( -LevelLower );
float2 LowerRectSize = ceil( LowerRect.zw ) - floor( LowerRect.xy );
if( all( LowerRectSize <= 4 ) )
{
        Level = LevelLower;
}LOD Calculate

每种Mesh的LOD相关参数随Cluster数据一开始传递给GPU,每帧ComputeShader根据Cluster对应屏幕占比计算LOD级别并统计到对应LOD数量。
IndirectBuffer Args Generator

最后将剔除完成的Cluster列表整理组织成对应的RenderIndex,提交给最后的DrawIndirect渲染。


测试结果

其他场景下测试(默认锁定60帧)


最后

这套系统离真正的GPU-Driven还是差了非常多,但即便这样比起原本的管线也有非常不错的收益。(天刀 yyds
参考


  • ^https://zhuanlan.zhihu.com/p/335325149
  • ^https://zhuanlan.zhihu.com/p/267179957

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-20 15:24 , Processed in 0.089655 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表