【cocos2dx】之RenderTexture实现截图

      大家好,我是Lampard

      今天与大家探讨一下cocos中实现截图所使用到的类RenderTexture

(一)使用RenderTexture实现截图功能

      cocos实现截图可以通过两种方式。第一种就是今天将要介绍的使用renderTexture类去实现,第二种是利用cocos2dx在3.2版本之后给我们提供的utils:captureScreen()实现截图。两种方式互有优劣,前者就好像你去到了一个超市,看见呈现在屏幕上琳琅满目的商品,你可以挑选任意商品绘制到你的图片上,但如果这个商品比较复杂(如3d的精灵)那么你可能就得不到想要的效果,需要花更多的功夫。而第二种使用起来就比较简单,就像小孩子才需要做选择,大人全都要。它能对屏幕直接进行截图,其实现原理有点手机截图,我不管你是怎么实现的,它直接把屏幕上的像素点记录下来再生成一个image保存,所以3d精灵对于第二种方式来说也不过是像素点而已,能够绘制出来,但它无法只画出你想要的内容

      我们可以用上文的方法实现截图功能,首先创建一个renderTexture画布,设置画布的大小。然后调用begin函数去记录期间渲染的对象(调用endToLua结束之前),最后输出到磁盘中

(二)解读RenderTexture源码

create方法

      我们通过查阅源码去了解上述方法的是如何实现截图的。在RenderTexture中,重载了好三个create方法,分别是:

  • create(int w, int h) -- 只传入宽高
  • create(int w, int h, Texture2D::PixelFormat eFormat) -- 传入宽高和客户端的像素格式
  • create(int w ,int h, Texture2D::PixelFormat eFormat, GLuint uDepthStencilFormat) -- 传入宽高,客户端的像素格式和深度模板缓冲格式

      我们是只传入了宽高,所以执行的是这一个函数。首先实例化了一个RenderTexture对象,然后调用了对象的initWithWidthAndHeight方法对画布进行一个初始化,最后把这个生成的对象加入自动回收池中,对于我们没有传入的客户端的像素格式和深度模板缓冲格式默认使用了RGBA8888和0

像素格式和深度模板缓冲格式是什么

      首先讲一下像素格式,像素格式常见的有4个通道,分别是:R红色通道,G绿色通道,B蓝色通道,A透明度通道。引擎默认是使用RGBA8888的格式,也就是每一个通道用8个位去记录值,所以一个像素就是需要32个位,也就是4个字节,如果一张1024*1024的图片就需要4M的空间去存储。所以常见的优化方式是把RGBA8888的像素格式修改成RGBA4444,对于精度要求没有那么高但是又需要透明通道的图片来说,能节省一半的空间。如果不需要透明通道,可以使用RGB565和RGB888等等,以下是官网像素格式的文档

      然后说一下什么是深度模板缓冲,它其实是包含了两个东西,分别是深度缓冲和模板缓冲

深度缓冲区

      深度缓冲区(或 z 缓冲区)存储像素的深度信息,以控制渲染哪些多边形区域。简单来说,就是当同一场景中,出现位置重叠的像素,那么GPU需要知道该显示哪一个像素信息,这时就可以通过深度缓存区中的值来比较,若比缓存区中的值小(更上层),颜色缓冲中记录下当前像素的信息,否则则不对此颜色进行记录

模板缓冲区

      模板缓冲区用于遮罩图像中的像素,以产生特殊效果。特殊效果包括合成、贴纸、溶解、淡化、滑动、轮廓描绘和剪影,以及双面模板等。模板测试发生在透明度测试(alpha test)之后,深度测试(depth test)之前。如果模板测试通过,则相应的像素点更新,否则不更新

      像上图中图一是颜色缓冲区,图二是模板缓冲区,图三就是最后最后渲染出的结果。cocos中深度模板缓冲格式给我们提供了三个选择

  • RB_FMT_D24S8:24 位深度缓冲和 8 位模板缓冲
  • RB_FMT_S8:只申请 8 位模板缓冲
  • RB_FMT_D16:只申请 16 位深度缓冲

initWithWidthAndHeight方法

      initWithWidthAndHeight方法主要是根据我们传入的宽高,客户端像素格式,深度模板格式对renderTexture对象进行一个初始化。若成功则返回true,不成功则返回false。它主要分以下几个步骤进行初始化

开辟空间传给纹理对象

      根据是否支持宽高纹理的像素为非2的n次方来调整设定的宽高,然后根据最终的宽高申请内存,大小为powW * powH * 4

      把申请得来的内存用0来填充,然后生成一个texture2D的纹理对象,并把内存块,内存长度,宽高等信息传递给纹理对象使其进行初始化

把这个纹理绑定到帧缓冲

      当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入到这个纹理中,就想它是一个普通的颜色/深度或模板缓冲一样。使用纹理的优点是,所有渲染操作的结果将会被储存在一个纹理图像中。

      创建帧缓冲对象用到了两个方法。glGenFramebuffers需要传入两个参数,第一个是要创建的帧缓存的数目,第二个是指向存储一个或者多个ID的变量或数组的指针。它返回未使用的FBO的ID。glBindFramebuffer第一个参数target是GL_FRAMEBUFFER,第二个参数是FBO的ID号从而生成FBO对象并把FBO对象绑定在上面,一旦FBO被绑定,之后的所有的OpenGL操作都会对当前所绑定的FBO造成影响最后是帧缓冲绑定的颜色缓存到纹理中

如果使用了深度缓冲,再创建一个深度渲染缓冲对象

      深度缓冲与帧缓冲类似,同样需要生成渲染缓冲对象然后绑定到GL_RENDERBUFFER中去。并且需要把这个渲染缓冲对象绑定在帧缓冲对象的GL_DEPTH_ATTACHMENT深度关联点中,如果还有模板缓冲则把这个渲染缓冲对象绑定在帧缓冲对象的GL_STENCIL_ATTACHMENT模板关联点中

 

把纹理数据作为参数,生成sprite绘制到屏幕帧缓冲中

      最后生成一个sprite,会过程中绘制的内容呈现在屏幕上,并把帧缓冲还原成最初的状态

帧缓冲是什么

      这个时候就会有朋友提问了,提了好几次帧缓冲,又得把深度模板的关联点绑定到上面去,那帧缓冲是什么东西呢?臣妾也不会啊,这只能去官网找点文档看了

      帧缓冲是用来存储渲染数据的地方,可以理解为显存。几何数据(顶点坐标、纹理坐标等)和纹理经过一系列渲染管道最终计算出屏幕上的所有像素点,它们需要一个地方来存放,这个地方就是帧缓冲。帧缓冲中的数据会被显示器读取来刷新显示。一个完整的帧缓冲需要有以下的条件

  • 附加至少一个缓冲(颜色、深度或模板缓冲)。
  • 至少有一个颜色附件(Attachment)。
  • 所有的附件都必须是完整的(保留了内存)。
  • 每个缓冲都应该有相同的样本数。

      当把一个纹理附加到帧缓冲的时候,所有的渲染指令将会写入到这个纹理中,就想它是一个普通的颜色/深度或模板缓冲一样。使用纹理的优点是,所有渲染操作的结果将会被储存在一个纹理图像中,我们之后可以在着色器中很方便地使用它。

      简而言之,颜色,深度,模板都只是其一个属性,当最终的结果出来之后就会存放在帧缓冲中,而使用纹理就可以把这次渲染的结果保存下来

begin方法

      begin方法首先是创建一个渲染队列组,用于记录之后之后截图所使用到的渲染指令,这样后续元素的RenderCommand将会被添加到_groupCommand中,从而实现分组绘制

  • cocos中的Renderer维护着一个RenderQueue数组,每隔RenderQuque记录了一组RenderCommand,每个RenderCommand通常由其globalOrder属性决定绘制顺序。
  • Renderer同时维护着一个RenderQueue的Id组成的栈,每个元素的绘制命令通过AddCommand发送到Renderer,Renderer会将其放置到Id栈中最后一个元素对应的RenderQueue上。
  • 一个GroupCommand在初始化的时候会创建一个新的RenderQueue并添加到Renderer的Id栈上,这样后续元素的RenderCommand将会被添加到新的RenderQueue,从而实现分组绘制,直到该GroupCommand绘制完毕将其从Id栈移除,从此不会影响后续的绘制命令。
  • 当所有的UI元素被遍历之后,Renderer会从RenderQueue数组的第一个RenderQueue开始绘制,如果某个RenderCommand的类型是GroupCommand,则找到该GroupCommand记录的RenderQueue开始绘制,从而实现了分组绘制。

      调整完正投影和可视窗口视口之后,然后在onBegin函数上再次使用glBindFramebuffer,指定渲染到渲染缓存,而不是渲染到屏幕上

visit&end方法

      上述步骤做完之后就可以把想要截图的对象重新“画”一次了,在cocos中我们可以调用其visit方法,visit方法中首先它根据localZOrder使用sortAllChildren()来对子节点进行排序,然后遍历将按照localZOrder从小到大调用draw方法,而draw方法则把它们押入到上文的渲染队列_groupCommand中去,然后每一帧再统一的把渲染队列中的节点按照GlobalZOrder的顺序进行渲染操作

      end方法中则是对帧缓冲和视图矩阵进行还原,然后把_groupCommand从渲染队列数组中pop出来,使之后的渲染指令能正常渲染在屏幕上

(三)测试截图功能

      我们就利用上次做的demo,把登录按钮的功能修改成截图看看效果

以上是学习路上的一点思绪,欢迎大家评论指点~


点赞,关注!!!

Logo

这里是一个专注于游戏开发的社区,我们致力于为广大游戏爱好者提供一个良好的学习和交流平台。我们的专区包含了各大流行引擎的技术博文,涵盖了从入门到进阶的各个阶段,无论你是初学者还是资深开发者,都能在这里找到适合自己的内容。除此之外,我们还会不定期举办游戏开发相关的活动,让大家更好地交流互动。加入我们,一起探索游戏开发的奥秘吧!

更多推荐