美文网首页
【GDC2010】Tessellation Performanc

【GDC2010】Tessellation Performanc

作者: 离原春草 | 来源:发表于2021-07-18 22:14 被阅读0次

这里分享的是AMD&NVIDIA工程师在GDC2010年上关于Tessellation Shader性能的技术分享,原文地址在参考文献[1]中给出。

首先,第一个问题,Tessellation是啥,为啥需要要使用Tessellation?

好吧,这是两个问题,Tessellation指的是在GPU中对面片进行拆分,以实现低模输入高模输出的技术。从低模到高模的处理同样可以放在CPU中进行,不过由于GPU端直接完成(硬件支持)性能更高;说到性能,为啥不直接采用高模作为输入?这是因为高模输入需要占用较多的带宽,较大的内存,而使用低模输入,则可以根据设备的性能进行程序化的Tessellation控制,以实现自适应的LOD。

Tessellation是在GPU上完成面片创建与模型细节丰富的技术,其流程图如上图所示:

  1. Vertex Shader对顶点的处理完成后,Input Assembler(IA)读取到低模的面片数据,将之传递给Hull Shader
  2. Hull Shader用于定义一个面片需要被分割成几个新的面片,分割的顶点位置在哪里,即指定面片分割规则(比如某条边分割成几份之类)
  3. Tessellator是硬件自行完成的固定管线Stage,在这个步骤中,GPU会根据上一步指定的分割规则在输入的面片上创建一系列新的顶点,并指定各个顶点的连接关系以生成新的面片列表。
  4. Domain Shader负责对新增加的顶点进行处理(比如根据displacement贴图调整顶点位置等),使之符合高模特征。
VS输出效果 DS输出效果

上面给出了VS跟DS输出效果的对比图,从图中可以看到VS输出的模型精度很低,而Tessellation则是从这些低精度数据中创建出高精度的模型出来,而从VS中读取数据而不是直接跳过VS进入Hull Shader的好处有如下几点:

  1. 可以在Vertex Shader之后,借用Vertex Transform的Cache来提升处理效率
  2. 可以借用Vertex Shader的Transform逻辑与Animation逻辑,以低精度的计算方式完成一些效果与模型精度无关的运算,从而避免浪费。
  3. 可以在Vertex Shader中完成Vertex Attributes的准备工作,这一部分也没有必要放在高模完成。

那么在这种机制下,看起来Tessellation的效率会十分之高,是否意味着这个处理过程是完全没有代价的呢?自然不可能……只要有计算,肯定就是有消耗的,消耗的高低取决于Tessellation的复杂度,因此在使用的时候要谨慎,只对那些真正需要进行曲面细分的模型开启这项功能,而且在使用的时候也要注意策略,下面来介绍一下有哪些常用的降低消耗的策略。

Tessellation Factor预判

曲面细分中有一个参数叫做Tessellation Factor,表示的是曲面细分的复杂度,对于每个输入的面片而言,不同的Tessellation Factor对应于不同数目的面片输出,两者的关系可以用下表表示:

从表中可以看到,当Tessellation Factor等于1的时候,输出的面片数就等于1,相当于没有进行任何Tessellation处理一样,很明显,在这种情况下Tessellation不但没有优化作用,还白白增添了三个Stage的浪费,因此,可以考虑在CPU的时候对模型的Average Tessellation Factor进行计算,当约等于1的时候,可以考虑放弃Tessellation处理,直接使用原始的VS-PS渲染逻辑。

Occlusion & Culling
正常渲染管线中,为了降低消耗,通常会将被其他物件遮挡的物体剔除掉,常用的方案有Occlusion Culling以及Early-Z Culling等,在Tessellation逻辑中,这种考虑就更为必要了,因为如果不做处理,浪费的幅度会更大。

除了上述剔除优化之外,Tessellation还有独属的优化策略,比如在Hull Shader中对输入的面片进行Frustum Culling,将相机范围之外的面片剔除掉(将Tessellation Factor设置为0即可),实现面片级别的Culling,这个策略的优化幅度与场景有关,在某个测试场景中达到了30%的优化幅度(当然,没啥借鉴意义)。

Adaptive Tessellation
前面说过,Hull Shader是可编程的,因此可以在实现的时候根据需要来控制Tessellation复杂度,比如我们可以根据如下三个参数来对Tessellation Factor进行动态调整:

  1. 基于距离的Tessellation,模型到相机的距离
  2. 基于角度的Tessellation,面片的朝向与相机视角的关系,如果相机视线与面片接近平行,那么可以考虑增加复杂度(即模型轮廓处面片需要密集一点,为什么?考虑一下一个十分粗糙的平面,在轮廓处顶点稀疏与浓密会有很大的差别)
  3. 基于密度的Tessellation,模型需要的顶点密度,比如某些法线变化幅度较高的区域,通常会需要较为密集的顶点,不过如何检测那些面片需要更高密度呢?难道是根据面片顶点法线?
  4. 基于面积的Tessellation,面片在屏幕空间的面积

过高的Tessellation复杂度对于游戏的性能有比较大的影响,因此在使用的时候可以考虑对上述参数进行组合,选取最为合适的Tessellation策略。

下面对上述策略进行一下展开介绍:

基于距离的Tessellation
这种Tessellation有如下两点需要注意:

  1. 在HS中根据面片到相机的距离来决定面片上每条边的Tessellation Factor
  2. 如果在CPU中开启了前面说的Tessellation Off逻辑(当平均Tessellation Factor接近1时)的话,在HS中也应该使用相同的Tessellation Factor Falloff Value来跳过Tessellation逻辑(目的是避免当CPU侧达到开启Tessellation条件时,开启前后的模型跳变过于剧烈)

基于距离的细分效果。

基于角度的Tessellation
对每条边,计算其平均法线:

edge_normal = ( point1_normal + point2_normal ) / 2;

之后计算平均法线与视线的点乘,根据这个结果判断当前面片的朝向,以此决定Tessellation Factor,点乘结果越大,视线与面片越接近垂直,Factor也就应该越小:

EdgeScale = 1.0f –abs( dot( N, V ) );

通常来说,Backface Culling是在光栅化阶段完成的,在这种Tessellation策略话,还可以将之提前到HS中完成。

这种策略尤其适合那些使用PN-Triangle(贝塞尔曲线通过在两个端点表示的直线中加入控制点实现曲线表达,与之对应的贝塞尔曲面则是在面片上添加控制点来生成平滑曲面,而PN-Triangle则是一种特殊的贝塞尔曲面细分方法,其内部增加的所有控制点都是根据输入三角形的顶点位置与法线插值得来,因此称为Point-Normal Triangles,简称PN-Triangle)方法来进行曲面细分的项目,原文给出的测试数据报名,在factor为9的时候,使用这种策略,其性能是(距离Tessellation方案?)的三倍。

上图是基于角度的Tessellation效果。

基于密度的Tessellation
前面好奇密度是如何确定,这里给出了一种方法,就是从displacement texture中预计算出每个面片(或者说顶点)的大致tessellation factor:

  1. 根据高度值,计算出displacement texture的gradients
  2. 根据gradients,输出对应的tessellation factor buffer
    上述工作在预计算的时候完成,在运行时,只需要在HS中根据每条边的uv,从buffer中采样就能够得到对应的tessellation factor了。

结果如上图所示。

基于面积的Tessellation
因为小面片(屏幕空间覆盖像素低于8个)在硬件光栅化的时候会有较大浪费,因此这里限制了每个面片在屏幕空间中cover的像素数目不得低于8个,而这个目标是通过限定每个面片的tessellation factor来达成的。

具体而言,可以将输入面片每条边在屏幕空间中的长度用于scale factor来判断当前面片是否是小面片,不过这种做法并不能覆盖所有的输入案例,在使用的时候需要仔细调整使用策略。

除了上述Tessellation Factor的使用策略之外,还有一些其他的Tessellation性能优化建议。

Draw Order
Tessellation开关是一个比较重的RenderStage切换逻辑,因此建议将所有的Tessellation物件放在一起绘制:

Stream Out
如果某个Mesh需要在同一帧中参与多次绘制(比如CSM的多级Shadow Map的绘制,又比如多盏光源的Shading逻辑等),那么如果这个Mesh需要使用Tessellation的话,建议只调用一次Tessellation,并将Tessellation结果通过Stream Out输出到CPU,之后直接使用这个Stream Out版本的Mesh来完成其他处理(说明Stream Out的消耗低于Tessellation的消耗?原文是说这个需要仔细测试,因为并不确保一定能存在优化……)。

通用法则
尽量将一些计算挪动到低频Stage中完成:

  1. CPU中完成,每帧一次
  2. Vertex Shader中完成,低模顶点计算频率
  3. Hull Shader中完成,低模面片计算频率
  4. Domain Shader中完成,高模顶点计算频率
  5. Pixel Shader中完成,像素级别计算频率

尽量减少传输到Pixel Shader的顶点属性,降低Stage之间的带宽消耗。

Hull Shader Tips

  1. 一个过于复杂的Hull Shader可能会导致性能瓶颈,因此HS要尽可能的简单,如果做不到,可以考虑将一部分工作挪动到Vertex Shader中完成
  2. 减少从Vertex Shader传输过来的顶点属性数据,降低带宽消耗
  3. 减少传递给Domain Shader的顶点属性,降低带宽消耗
  4. 指定HS的maxtessfactor()参数,这样硬件可以相应的进行一些处理来降低负载
  5. 推荐使用PASS-THROUGH Control Point Phase(指的是mesh的每个面片都采用相同的tessellation策略吗?),只需要一个硬件线程即可完成。

DS Shading
可以测试下,如果将一些低频的shading逻辑从Pixel Shader提前到Domain Shader中来进行,是否有助于降低整条渲染管线的消耗。比如将环境光、体积光等计算放到DS中,不过实际上这种操作在屏幕空间的均匀划分(比如输入),通常对于那些基于屏幕空间面片面积的tessellation方案效果比较好。

上图给出了完全在Domain Shader中完成的水面焦散(Caustic)渲染的效果,上图Tessellation Factor较小,下图Factor较大。

这是另一个在Domain Shader中完成的Shading案例,Fourier Opacity Maps(FOM)实现。

使用Fourier Series(傅里叶级数)来模拟实现的Volumetric Shadow会有比较高昂的消耗,尤其是当参与计算的光源数目较多的时候。而这种情况尤其适合DS Shading方案,将FOM相关计算逻辑从PS移动到DS,可以以较低的计算频率完成volumetric soft shadow的计算。其他的逻辑比如billboard texture lookup以及光照shading计算都还放在PS中完成。

在DS Shading逻辑下,我们可以很轻易的实现variable rate shading(VRS),比如我们只需要控制tessellation factor就能控制对应位置的shading rate了。

Domain Shader Tips
跟HS一样,Shader越简单越好,否则在高复杂度的Tessellation Factor作用下,可能会导致性能瓶颈。

在采样displacement map的时候,建议使用mipmap采样,避免破坏贴图读取的缓存效率。

减少传递给GS或者PS的顶点属性数据,降低带宽消耗。

参考文献

[1] GDC2010 - Tessellation Performance

相关文章

网友评论

      本文标题:【GDC2010】Tessellation Performanc

      本文链接:https://www.haomeiwen.com/subject/dgvipltx.html