unity游戏开发之移动平台下的渲染优化

最近学习unity shader,发现很多提到怎么优化shader的一些概念,但是都是零零散散, 感觉总是少点什么,似懂非懂的感觉,因此梳理了一下最近学习到的一些知识,分享给大家。

对渲染流程理得不太清楚的,可以看一下我的另一篇文章,快速了解渲染管线:

https://blog.csdn.net/qq_17347313/article/details/105028493

  • 移动平台的特点:

移动平台下,有着跟PC端不一样的特点,造成差异的主要主要原因是受限于移动端体积,与能耗的问题。因此移动端GPU架构主要要解决功耗问题,功耗意味着发热量,意味着耗电量,意味着芯片大小。然而在GPU中最影响功耗是bandwidth,也就是我们常说的带宽,即每秒钟读写数据的数量。

对于图形处理来说,主要是两点:

第一,是有限的带宽。实际上,要增加计算能力,在功耗允许的情况下,堆核心并不是一件难事,事实上我们也看到了不少SOC集成了四核乃至“16核”GPU。但是难点在于,需要有足够的带宽去满足这颗强大的GPU,避免其出现“饿死”的情况。在左图的移动平台中,CPU、GPU和总线被共同集成在一颗芯片上,称之为SOC。整个SOC,包括其中的CPU和GPU,共享有限的内存带宽。即使是相对高端的,采用64bit内存位宽的一些SOC,如三星4412,高通8064等,也只是6.4 - 8.5GB/s的带宽,相比起PC平台主内存十几GB/s的带宽,和PC GPU GDDR5显存动辄几十,不少都超过100GB/s的带宽,只能说是少的可怜。相对另类的苹果在iPad4中,给A6X芯片搭配了128bit的LPDDR2-1066,带宽达到了17GB/s,用以喂饱强大的SGX 554 MP4 GPU,但相比PC平台依旧是小巫见大巫。因此,移动平台要在有限的带宽下实现合理的性能,在不少时候,瓶颈可能并不在于计算能力上,而在于带宽上。

第二,相比PC平台的CPU,移动平台的CPU浮点较弱,在Cortex-A9开始虽然有所好转,但64bit的NEON跟桌面128bit甚至256bit的SIMD还是有显著差距,外加主频的差别。因此更多的计算也依赖硬件Vertex Shader去完成。

针对这个问题,现在不同的芯片厂家提供了不一样的解决方案:

厂商

芯片

手机

GPU架构

ImgTec

PowerVR

苹果

TBDR

Qualcomm(高通)

Adreno

晓龙Soc

TBDR

ARM

Mail

三星、MTK、Mstar

TBIMR

首先介绍一下什么是IMR(Immediate Mode Rendering):

    目前几乎所有的桌面GPU(nVIDIA,AMD)都是IMR架构,在移动领域,nVIDIA的GeForce ULP和Vivante的GC系列GPU都是属于IMR架构。IMR架构的GPU渲染完物体后,都会把结果写到系统内存中的帧缓存里,因此就可能出现GPU花了大量的时间渲染了一个被遮挡的看不见的物体,而最后这些结果在渲染完遮挡物后被覆盖,做了无用功。这个问题称之为Overdraw。虽然现代的IMR架构GPU在一定程度上可以避免这个问题,但要求应用程序将场景里的三角形按照严格的从前往后的顺序提交给GPU,要完全避免Overdraw还是很困难的。另一方面,由于IMR架构的GPU频繁的读写和修改帧缓存,因此对带宽的要求比较高,同时也增加了电力的消耗。

    Mali的渲染架构相对简单,属于TBIMR,也就是分块式立即渲染,对递交的三角形顺序不作筛选处理,分块的主要目的是节省带宽,不过它的某些型号具备名为 Forward Pixel Kill的技术,能够随时停掉“预见”到的无效像素(线程)的渲染,号称可以达到媲美PowerVR TBDR 的效果。同时,Mali采用了“完整弱GPU内核”(相对于其他厂商的“完整可延伸GPU内核”而言)设计,内核中的渲染单元不能扩展,性能的规模延伸是透过增加内核数来达到。

 

然后介绍一下什么是TBDR(Tile-Base-Deffered-Rendering):

GPU在渲染的时候,一般是逐个三角形渲染的,如果以深度顺序来划分,可以分为从远到近(画家画法)、从近往远,开发人员可以在编写代码的时候指定渲染顺序。

    讲一下为什么需要Tile(瓦片):之前有提到,移动端需要节省带宽,而主要使用带宽的是像素着色器(有的叫片段着色器),而顶点着色器对带宽的使用相对来说可以忽略,理想情况下,如果我们能在顶点着色器的时候就剔除掉那些不需要计算的像素,只计算我们看得到的那些像素,就可以大大减小带宽的使用。通常gpu的onchip memory(也就是SRAM,或者L1 L2 cache)很小,这么大的FrameBuffer要存储在离gpu相对较远的DRAM(显存)上,像素着色器频繁的访问必然造成能耗问题,因此使用Tile技术在把三角形场景变成像素图(光栅化)前,先把整个画面分成小块,先把这些小块的渲染在GPU上的高速缓存里进行(GPU的缓存)读写,而不是对FrameBuffer(位于手机运行内存里)的频繁读写,然后将渲染后的数据一次性写到FrameBuffer。当然,不同的GPU分块的大小也有所不同,PowerVR和Mali一般是16*16像素的块大小,而大部分的高通Adreno都带有256K的缓存,以256K作为块的大小进行渲染,高通称之为binning。

    为什么需要Deffered(延迟渲染):Tile只是更改从显存(移动端就是运行内存)到GPU的数据读取方式达到降低带宽的目的,但是无法减少我们需要渲染的规模。其实存在很多被遮挡的图元,我们根本看不到,但是也要进行像素着色器,这样,我们计算的像素并没有减少,来一个画一个,那么gpu可能会在每一个drawcall上都来回搬迁所有的Tile,这肯定不能忍。针对这个问题芯片一般都会有一个HSR(Hidden Surface Removal))硬件单元,在进行完顶点着色器(VS)之后,不会直接进行后面的流水线。会先通过对一个块内的三角形进行测试,剔除掉被遮挡的三角形,合成一幅由所有可见部分组成的画面,交给后续的流水线去渲染。这样不可见部分就不需要Pixel Shader去做相应的计算,也不需要去拾取相应的纹理,节省了计算量的同时也节省了带宽,对移动设备来说有很大的帮助。

 

优缺点分析:

为什么PC端可以直接使用IM呢,因为PC端有自己的显存,而不像移动端需要共享运行内存,也不存在能耗的问题,因此使用IM模式可以实现快速的绘制。快速的原因在于直接读写。

而移动端因为能耗,带宽问题,采用TBDR可以大大降低能耗,节约计算资源。

貌似说了一堆废话,其实看完以后,你会清楚我们每次遇到的问题瓶颈具体在哪里,就知道怎么去解决了。

 

性能瓶颈点:

  • CUP上一般就是DrawCall
  • GPU上一般是顶点着色器与像素着色器,也就是顶点、像素数据规模,计算复杂度
  • 带宽上一般是对纹理的尺寸,采样数,缓存命中率

 

优化方案:

Drawcall: 

    drawcall在之前的文章中说过,大家应该也很熟悉,每次调用图形渲染命令的时候都会调用一次drawcall,过多的drawcall会造成CPU的性能瓶颈,因为每次drawcall cpu需要改变很多渲染状态,这些操作是非常费时的。如果drawcall多,cpu把大部分时间都花费在了提交drawcall上面,造成了卡顿。

    我们减少drawcall的思路很简单,那就是合批,提交多次的内容合成一次就好了。具体怎么合批呢?unity中有2中合批方式:

静态合批:

    在运行开始阶段,把需要静态合批的模型合到一个新的网格结构中,只合批一次,因此意味着一旦合批,再也不能改变(移动),由于只合批一次,因此比较高效,但是缺点就是会占用更大的存储空间,因为每一个模型的数据都要在新的网格结构中有一个备份。

动态合批:

    区别于静态合批,动态合批每一帧都要对需要进行合批的网格进行合并,但是unity要求合批的网格使用的是同一张材质,还有这一些限制,毕竟每帧计算是比较耗的。限制条件:

  • 同材质
  • 顶点规模小于900(会改变),包括位置,法线,纹理坐标都算。
  • 多pass shader会中断批处理
  • 如果GameObjects在Transform上包含镜像,则不会对其进行动态合批处理

github 上Unity官方总结了25种不能被合批处理的情况, Unity-Technologies/BatchBreakingCause

 

减少顶点规模

    减少顶点规模,后续所有的计算都会减少。

  • 因此我们在建模时,我们就要注意减少模型中是三角形面片的数目(一般是美术做),或者肉眼难以察觉的顶点尽量去掉,在很多3D建模软件中,都有一些优化选项,直接优化网格结构的。避免一些边界平滑和纹理分离。
  • 其次,可以使用LOD(Level of Detail)技术,其实就是根据距离摄像机的距离,减少模型上的片面数量。
  • 使用遮挡剔除(Occulusion culling)。unity使用一个虚拟摄像机遍历整个场景,从而构建一个可见对象的集合。运行时刻,摄像机会使用这些数据识别哪些物体是可见的,哪些不可见。

 

减少片元数量

  • 控制渲染顺序,避免overdraw,
  • 透明物体处理,减少使用discard,clip操作,因为会导致一些硬件优化失效。针对之前我们所说的TBDR中延迟渲染的部分,discard,clip操作是在像素着色器才知道是否被渲染的,因此无法在图像VS之后就知道,所以这段的优化就被去掉了。
  • 减小实时光照跟阴影。原因是这种逐像素光源无法合批,被光照的物体要被渲染2次,一次阴影,一次本身的颜色。unity中可以尽量使用烘焙把阴影信息提前计算到一张lightmap中,实时光可以选择使用逐顶点代替。或者可以考虑使用查找纹理(LUT look up table)

节约带宽

  • 尽量使用纹理长宽2^{n}的正方形纹理
  • mipmapping技术,unity中自带直接在纹理上勾选 generate  mip maps
  • 分辨率缩放
  • 记得不使用Framebuffer的时候clear或者discard,原因已经说过了。
  • 在每帧渲染之前尽量clear,在一些渲染技巧里面可能每帧渲染前不clear当前的buffer,例如认为下一帧一定会把整个fb都写掉,就没必要先clear了。这是对于pc显卡来说的,对于tbr架构这就是个噩梦了。因为如果不clear,那么每个tile在初始化的时候都要从dram上的framebuffer把那一块的内容完整拷贝过来,而clear这个初始化就变得非常简单了。
  • 不要在一帧里面频繁的切换framebuffer,tbdr的架构,gpu会尽可能的只在不得不绘制的时候才渲染framebuffer。假设这样一种情况,你先在fb上绘制a,然后使用fb,然后绘制b,再使用fb,再绘制c,再使用fb,这对于pc显卡问题不大,但是移动设备上每次使用fb都会触发一个所有tile的绘制,如果我们能尽量做到绘制a,b,c后再一起使用fb就好了。
  • 合并纹理通道

 

降低计算复杂度

  • 使用shader的LOD技术,只有shader的LOD值小于某个设定的值,这个shader才会被渲染
  • 代码优化,对象数 < 顶点数 < 像素数,因此在不大影响画质的前提下,数据计算前移。
  • 减少分支 循环的使用
  • 避免复杂的数学计算,转换为常用的多项式函数,或者查表

 

关于Early-z

    Early-Z在GPU渲染流水线中位于三角形遍历和像素着色器(含纹理单元)之间,它会读取并测试片元的Z深度值,在进入像素着色单元之前抛弃掉不可视的片元。

    Early-Z只有在三角形按照从前往后(由近及远)的递交顺序才能起作用,而且为了做到片上高速处理,一般会采取层次化Z缓存(Hierarchical Z-buffer)技术。

    在采用Hierarchical Z-buffer的时候,GPU 会把每2×2个片元的四个 Z值中最接近屏幕或者眼睛的Z值取出来,存到一个更“粗糙”的版本。这个更“粗糙”版本的Z-buffer分辨率只有原 Z-Buffer的四分之一,GPU再对这个“粗糙”版本的Z-buffer再做同样的事,一直重复直到分辨率只有一个片元。这样一来,就形成一个pyramid(金字塔)。在渲染片元的时候,GPU先比较最粗糙版本的Z-buffer。如果要画的片元Z值还比较远,那这个片元就一定不用画出来了。如果它比较近,就再拿比较细的Z-buffer来比较,一直比较到最细的版本。在理想的情形下,通常大部分的片元都不需要比较到最细的版本,所以可以节省不少时间(要记得,一个“粗糙”版的Z值比较,其实就等于和四个“精细”版的 Z 值比较)。

 

    以上是最近看到的一些优化方案,当然不会面面俱到,可能会漏掉一些,之后会补充。

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值