RenderTexture
Unity:https://docs.unity3d.com/Manual/class-RenderTexture.html
什么是RenderTexture
Unity3D中有一种特殊的Texture类型,它本质上是将一个FrameBufferObjecrt连接到一个server-side的Texture对象中。
什么是server-side的texture
在渲染过程中,贴图最开始是存在CPU(中央处理器)的内存中的,这个贴图我们通常称为client-side的texture,它最终要被送到GPU的存储里,GPU才能使用它进行渲染,送到GPU里的那一份被称为server-side的texture。这个texture在CPU和GPU之间拷贝要考虑到一定的带宽瓶颈。
什么是FrameBufferObject
可以理解FrameBufferObject是一个集合,集合了FrameBuffer,通过快速刷新Framebuffer实现动态效果,最典型的FBO就是Unity的Main Camera,它是默认的FBO,是GPU里渲染结果的目的地.但是现代GPU通常可以创建很多其他的FBO(Unity中可以创建多个Camera),这些FBO不连接窗口区域,这种我们创建的FBO的存在目的就是允许我们将渲染结果保存在GPU的一块存储区域,待之后使用,这种用法叫做离屏渲染,这是一个非常有用的东西。
Camera 输出的FBO,可以嵌在另一个FBO中,Unity中使用RenderTexture来接收FBO(可视化FBO),game窗口就是一个RenderTexture,当Camera的RenderTarget设置为null时表示输出到game窗口(没有摄像机的RenderTaget为null会显示没有摄像机进行渲染),设置不为null表示输出到某个RenderTexture.
如何使用FrameBufferObject
当渲染的结果被渲染到一个FBO上后,就有很多种方法得到这些结果,我们能想想的使用方式就是把这个结果作为一个Texture的形式得到,通常有这样几种方式得到这个贴图:
●将这个FBO上的结果传回CPU这边的贴图,在GLES (可编程着色功能的图形管线) 中的实现一般是ReadPixels()这样的函数,这个函数是将当前设为可读的FBO拷贝到CPU这边的一个存储buffer,没错如果当前设为可读的FBO是那个默认FBO,那这个函数就是在截屏,如果是你自己创建的FBO,那就把刚刚绘制到上面的结果从GPU存储拿回内存。
●将这个FBO上的结果拷贝到一个GPU上的texture,在GLES中的实现一般是CopyTexImage2D(),它一般是将可读的FBO的一部分拷贝到存在于GPU上的一个texture对象中,直接拷到server-sider就意味着可以马上被GPU渲染使用
●将这个FBO直接关联一个GPU上的texture对象,这样就等于在绘制时就直接绘制到这个texure上,这样也省去了拷贝时间,GLES中一般是使用FramebufferTexture2D()这样的接口。
那么unity的RenderTexture正是第三种方式的一种实现,它定义了一个在server-side的texture对象,然后将渲染直接绘制到这个texture上。
RenderTexture的用途
-
屏幕后处理,3D游戏最基本的后处理是抗锯齿,从Unity的FrameDebugger中可以看到抗锯齿的操作在OverlayUI之前,所以各位做2d游戏的可以选择把抗锯齿关掉,其他的后处理如bloom,HDR等都是操作屏幕这个默认的RenderTexture,配合上相关效果的Material
-
在Scene中直接将RenderTexture作为Texture传给其他材质球,操作是调用Material.SetTexture 为该RenderTexture,即可实现在另一个表面渲染另一个Camera的内容.可以制作后视镜功能
-
copy回CPU端的内存:基本操作是在当前帧渲染完毕后(协程中, yield return new WaitForEndOfFrame()),设置RenderTexture.active为目标RenderTexture(因为当前帧已渲染过,所以该RenderTexture不会被渲染).Texture.ReadPixels保存到显存.Texture.GetRawTextureData()读回CPU内存,可以保存到硬盘或者通过互联网通信(在unity中实现的截屏,录屏,实时共享屏幕).
以上2,3都属于离屏渲染的应用.
渲染到RenderTexture的几种方式
- 在assets里创建一个RenderTexture,然后将其附给一个摄像机,这样这个摄像机实时渲染的结果就都在这个RenderTexture上了。
- 有的时候我们想人为的控制每一次渲染,你可以将这个摄像机disable掉,然后手动的调用一次render。
- 有的时候我们想用一个特殊的shader去渲染这个RenderTexture,那可以调用Camera的RenderWithShader这个函数,它将使用你指定的shader去渲染场景,这时候场景物体上原有的shader都将被自动替换成这个shader,而参数会按名字传递。这有什么用?比如我想得到当前场景某个视角的黑白图,那你就可以写个渲染黑白图的shader,调用这个函数。(这里还有一个replacement shader的概念,不多说,看下unity文档)
- 我们还可以不用自己在assets下创建RenderTexture,直接使用Graphics.Blit(src, target, mat)这个函数来渲染到Render Texture上,这里的的target就是你要绘制的Render Texrture,src是这个mat中需要使用的_mainTex,可以是普通tex2d,也可以是另一个rendertexrture,这个函数的本质是,绘制一个四方块,然后用mat这个材质,用src做maintex,然后先clear为black,然后渲染到target上。这个是一个快速的用于图像处理的方式。我们可以看到Unity的很多后处理的一效果就是一连串的Graphics.Blit操作来完成一重重对图像的处理,如果在CPU上做那几乎是会卡死的。
从rendertex获取结果
大部分情况我们渲染到RenderTexture就是为了将其作为Texture继续给其他mat使用。这时候我们只需把那个mat上调用settexture传入这个Texture就行,这完全是在GPU上的操作。
但有的时候我们想把它拷贝回CPU这边的内存,比如你想保存成图像,你想看看这个图什么样,因为直接拿着RenderTexture你并不能得到它的每个pixel(像素)的信息,因为他没有内存这一侧的信息。Texture2d之所以有,是因为对于选择了read/write属性的tex2D,它会保留一个内存这边的镜像。这种拷回就是一部分写的a)方式,把RenderTexture从gpu拷贝回内存,注意这个操作不是效率很高。copy回的方法通常是这样的
Texture2D uvtexRead = new Texture2D()
RennderTexture currentActiveRT = RenderTexture.active;
// Set the supplied RenderTexture as the active one
RenderTexture.active = uvTex;
uvtexRead.ReadPixels(new Rect(0, 0, uvTexReadWidth, uvTexReadWidth), 0, 0);
RenderTexture.active = currentActiveRT;
上面这段代码就是等于先把当前的FBO设为可读的对象,然后调用相关操作将其读回内存。
其他的一些问题
-
rendertexture的格式,rt的格式和普通的tex2D的格式并不是一回事,我们查阅文档,看到rt的格式支持的有很多种,最基本的ARGB32是肯定支持的,很多机器支持ARRBHALF或者ARGBFLOAT这样的格式,这种浮点格式是很有用的,想象一下你想把场景的uv信息保存在一张图上,你要保存的就不是256的颜色,而是一个个浮点数。但是使用前一定要查询当前的gpu支持这种格式
-
如果你想从RenderTexture拷贝回到内存,那么rt和拷贝回来的tex的格式必须匹配,且必须是rgba32或者RGBA24这种基本类型,你把float拷贝回来应该是不行的
-
rendertexture的分配和销毁,如果你频繁的要new一个rt出来,那么不要直接new,而是使用RenderTexture提供的GetTemporary和ReleaseTemporary,它将在内部维护一个池,反复重用一些大小格式一样的rt资源,因为让gpu为你分配一个新的tex其实是要耗时间的。更重要的这里还会调用DiscardContents
-
DiscardContents()这个rendertex的接口非常重要,好的习惯是你应该尽量在每次往一个已经有内容的rt上绘制之前总是调用它的这个DiscardContents函数,大致得到的优化是,在一些基于tile的gpu上,rt和一些tile的内存之间要存在着各种同步, 如果你准备往一个已经有内容的rt上绘制,将触发到这种同步,而这个函数告诉gpu这块rt的内容不用管他了,我反正是要重新绘制,这样就避免了这个同步而产生的巨大开销。总之还是尽量用GetTemporray这个接口吧,它会自动为你处理这个事情