NoiseFloor 发表于 2023-3-4 20:44

Unity中GrabPass、OnRenderImage、TargetTexture性能测试

我们在游戏中经常用需要用到屏幕的后处理效果(类似:泛光效果,热浪扭曲,景深模糊等等),需要实现这些效果都避免不了的要拿到当前相机渲染的画面,然后当做纹理贴图去传递给shader做一系列的计算。首先要了解下RenderTexture,这里有篇文章讲述的比较清楚了。
在Unity中我们主要通过以下方法能够拿到当前相机渲染的画面:
1.GrabPass,这个是Shader中的一个特殊Pass
2.OnRenderImage,常见的后处理效果都是在这里面去执行的
3.TargetTexture,直接设置相机的TargetTexture
接下来我们在真机上测试下以上实现方法的性能,测试机选用的是ViVO X5ProV。测试的场景也很简单,就是渲染一个九宫格的贴图,没有任何光照计算,把相机渲染的九宫格画面当做贴图传递给一个面片来显示。RenderTexture的分辨率为1920x1080,16位Depth Buffer。


1.GrabPass
在Shader中加上GrabPass,每帧都会抓取一张当前屏幕的画面存储在“_GrabTexture”里面,也可以通过GrabPass{“_GrabTempTex”}来自定义名称。没有优化过的特效实现扭曲效果的Shader用的就是这个方法。


我们可以看到,只是通过GrabPass做一个单纯的截屏,在低端的移动设备上就已经开销很大了,所以在很多游戏做机型适配的时候,低端机都是直接阉割掉扭曲特效的。猜想可能造成的原因是把Back-Buffer拷贝到_GrabTexture这个过程开销很大。
2.OnRenderImage
OnRenderImage是Camera的一个回调,相机每次渲染后都会调用,官方给的后处理方法都是用这个来实现的。这里我们新建一个C#脚本,然后挂在相机上,只做一个单纯的Graphic.Blit操作。
private void OnRenderImage(RenderTexture source , RenderTexture destination)
    {
      Graphics.Blit(source , destination);
    }
source就是当前相机渲染的画面,destination是最后输出到屏幕的画面。这里省略了后面的material参数,只是单纯的把source拷贝到destination作为输出。


可以看到这种操作会比GrabPass的性能好很多,但是只能固定在相机渲染完成之后调用,使用起来很不灵活。
3.TargetTexture
申请一个新的RenderTexture,然后把Camera.targetTexture设置为新的RenderTexture,再把这个RenderTexture作为贴图传递给shader进行计算。
private void OnEnable()
    {
      cam = GetComponent<Camera>();
      RenderTexture.ReleaseTemporary(screenCopyRT);
      screenCopyRT = RenderTexture.GetTemporary(cam.pixelWidth , cam.pixelHeight , 16);
      Shader.SetGlobalTexture("_GrabTempTex" , screenCopyRT);
      cam.targetTexture = screenCopyRT;
    }
这里的Shader还是用之前GrabPass时的,所以传递的贴图名称"_GrabTempTex"是一样的,但是要在Shader中把GrabPass这段注释掉,不要调用这个方法,其他的都不变。


如果考虑到实际项目中的应用,应该只有这种方案在低端机上能够勉强接受了,但是这种方案需要多一个相机,因为当前相机设置了TargetTexture后,在屏幕窗口就是全黑色的画面了。可能是设置了TargetTexture后,渲染的内容就存在了这张RenderTexture上面,而Back-Buffer中就没有内容显示了,要加多一个相机把处理后的内容在输出到Back-Buffer。
在Unity的论坛里面,也有看到一种不需要两个相机的实现方案,在OnPreRender的时候设置Camera的targetTexture,然后在OnPostRender中又把targetTexture设置为null,还要加上Graphics.Blit(myRenderTexture,null as RenderTexture),也就是说如果Blit的目标为null就会直接输出到Back-Buffer。
RenderTexture myRenderTexture;
void OnPreRender()
{
    myRenderTexture = RenderTexture.GetTemporary(width,height,24);
    camera.targetTexture = myRenderTexture;
}
void OnPostRender()
{
    camera.targetTexture = null;
    Graphics.Blit(myRenderTexture,null as RenderTexture, postMat, postPassNo);
    // Whatever other blits you may need
    RenderTexture.ReleaseTemporary(myRenderTexture);
}
这种方案我在PC端测试过是可以实现的,但是在移动端不同的机型上会有不同的显示Bug,所以不建议在移动端这样使用。
最后说一下CommandBuffer,这个用起来非常的灵活,可以在指定的时间去执行一系列的渲染指令。CommandBuffer也可以将当前相机渲染的内容拷贝到一张RenderTexture上,但是性能开销也很大,跟GrabPass差不多了,所以也不建议通过CommandBuffer.Blit(BuiltinRenderTextureType.CurrentActive , RenderTexture)这样来保存屏幕画面的内容。
后面的工作就是要通过TargetTexture和CommandBuffer,来整合一套屏幕后处理效果的实现方案了。

TheLudGamer 发表于 2023-3-4 20:46

你最后那个有将内容输出到屏幕吗?

ainatipen 发表于 2023-3-4 20:46

都是拷贝成本,差异就只会在切换渲染目标的次数上,而每次Graphics.Blit都会强制中断一次渲染,是Blit比较慢的原因,但是如果上下文强相关就无法避免。而扭曲本身必须要一次上下文强相关的切换。
所以要看的是Frame Debuger里具体执行了哪些指令,如果效率低必然是执行了多余的指令。

优化Blit的方法是用drawTri代替Blit,但这样则要保证上次绘制好的图像中间不再被使用。

上下文强相关的意思两条绘制指令必须等上一条完全执行后才能执行下一条,而不强相关的话,是允许不同绘制区域错开时间执行的。

你做测试最好稍微模拟下真实环境,就是放一个Cude实际渲染一下,并且不要舍弃不必要的步骤,比如你Cude如果要扭曲是需要绘制一个扭曲物体的并且叠在原来的图像上的(或者先绘制扭曲通道并根据通道图扭曲),你这又不是全屏扭曲,不能省。

xiangtingsl 发表于 2023-3-4 20:50

我这个测试只是为了看看把当前相机渲染的画面拷贝到RenderTexture上几种方式的效率而已。不是应该屏蔽掉其他的计算和影响吗?

super1 发表于 2023-3-4 20:52

CommandBuffer方式还没提呢

Baste 发表于 2023-3-4 20:58

新管线的CameraOpaqueTexture 是什么情况呢?

量子计算9 发表于 2023-3-4 20:58

仅仅是拷贝的话怎么性能差那么多呢?
页: [1]
查看完整版本: Unity中GrabPass、OnRenderImage、TargetTexture性能测试