|
简述
传统渲染管线在渲染时的最小剔除粒度是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
|
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|