绘制到其他渲染目的地 译文

framebuffers是渲染命令的目的地。当你创建一个framebuffer对象,你就对color、depth和stencil data 精确控制。你通过添加图片附件给framebuffer提供数据,如图4-1所示。最常见的图像附件是一个renderbuffer对象。你也可以给framebuffer的color attchmeng附加一个OpenGL ES纹理,这意味着任何绘图命令渲染到纹理上。稍后,纹理可以作为对将来渲染命令的输入。你也可以给一个渲染context创建多个framebuffer。你可以这样做,这样多个framebuffer就可以共享相同的渲染管线pipeline和OpenGL ES 资源。

图片发自简书App

所有这些方法都需要手动创建framebuffer和renderbuffer来存储你的渲染结果,以及写额外的代码来展示他们的内容到屏幕和(如果需要)个动画循环。

创建一个framebuffer对象

根据你的app 要完成什么样的任务,你的app 需要给framebuffer设置不同的附件对象。大多数情况下,配置framebuffer 不同之处在与 给一个framebuffer对象的color attachment 设置不同的对象。

  • 如果framebuffer是用来做offscreen image处理 设置renderbuffer。
  • 如果framebuffer image用来作为下一步渲染的输入,设置一个纹理texture。
  • 如果framebuffer用在核心动画的layer组件,设置一个特殊的核心动画 aware renderbuffer。

创建offerscreen framebuffer对象

一个用来做offscreen渲染的framebuffer对象分配它的所有附件空间为OpenGL ES renderbuffer。

  1. 创建一个framebuffer 并绑定
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
  1. 创建一个color renderbuffer 分配内存,然后设置到framebuffer的color attachment。
GLuint colorRenderbuffer;
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);
  1. 创建一个depth或者depth/stencil renderbuffer 分配内存,设置到framebuffer的depth attachment。
GLuint depthRenderbuffer;
glGenRenderbuffers(1, &depthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, depthRenderbuffer);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthRenderbuffer);
  1. 检测framebuffer的完成性。只有当framebuffer配置改变的时候才需要检测。
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER) ;
if(status != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"failed to make complete framebuffer object %x", status);
}

当绘制到一个offscreen renderbuffer之后,你可以使用glReadPixeks接口把绘制的内容返回给CPU ,用来下一步的处理。

用framebuffer对象来渲染到纹理

创建framebuffer对象的代码都是一样的,只是这次是创建一个纹理 添加到 color attachment上。

  1. 创建framebuffer
  2. 创建纹理,添加到framebuffer color attachment上
// create the texture
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8,  width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
  1. 创建 并添加depath buffer
  2. 检测framebuffer完整性

虽然这个例子 假设你渲染到 color texture,但是其他也是可以的。例如,使用OES_depth_texture 你可以添加一个纹理到depath attachment来存储深度信息。你可以用深度信息来计算阴影。

渲染到核心动画layer

核心动画是iOS上图形渲染和动画的核心基础设施。你可以用layer组成你的app 的界面或者其他视觉显示,layer上有使用不同机制渲染过的内容,渲染机制有 UIKit,Quartz2D,OpenGL。OpenGL ES 连接核心动画通过caeagllayer类,是一种特殊类型的核心动画layer,这个layer的内容来自一个OpenGL ES的renderbuffer 。核心动画混合这个renderbuffer的内容和其他的layer 然后最终展示在显示器上。


图片发自简书App

这个CAEAGLLayer类提供两个关键的功能来支持OpenGL。首先,它为renderbuffer分配共享存储。其次它把renderbu 提交到核心动画,从缓存数据替换层之前的内容。这种模式的一个优点是,核心动画层的内容不需要被绘制在每一帧中,只有当所呈现的图像变化才需要。

GLKView类自动做了下面的步骤,所有当你想用OpenGL 绘制 view 的layer的内容时候,你应该使用这个类。

使用OpenGL 类渲染核心动画layer:

  1. 创建一个GAEAGLLayer对象然后设置它的属性。 为了最优的性能,设置opaque为yes,通过把新字典设置到drawablePropertues属性,来配置渲染界面的界面属性。你可以设置renderbuffer的像素格式,设置renderbuffer的内容是否丢弃当提交到核心动画后。
  2. 创建一个OpenGL 上下文,并设置到当前线程上下文中。
  3. 创建framebuffer对象
  4. 创建一个color renderbuffer,调用`renderbufferStorage:fromDrawable:方法并把layer当参数来分配内存。宽高像素格式信息都从layer上获取。
GLuint colorRenderbuffer;
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:myEAGLLayer];
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);

注意:当你修改layer的bounds或者其他属性,你的app要重新分配renderbuffer的内存。如果你不重新分配,renderbuffer的大小会不匹配layer;这种情况下,核心动画可能会计算image的内容来适应layer。

  1. 获取corlor renderbuffer的宽高
GLint width;
GLint height;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);

在以前的例子中 renderbuffer的宽高是明确的去分配buffer的内存。这个例子中,是在color renderbuffer分配后采取获取宽高。这是因为color renderbuffer的实际尺寸是根据layer的bounds和scale来计算的。其他的framebuffer的renderbuffer必须有一样的尺寸。另外,用宽高来分配depath buffer 设置OpenGL view port 帮助决定你的app 纹理和模型要求的颗粒等级。

  1. 分配设置depth buffer
  2. 检测完整性
  3. 使用adddSubLayer:方法把CAEAGLLayer对象添加到核心动画layer层级中。

绘制到framebuffer对象中

现在你有一个framebuffer对象,你需要填充它。这节讲述 渲染新的帧,展示给用户的步骤。渲染到一个纹理或者offscreen framebuffer 大体一样,除了你的app 怎么使用最后一帧。

按照命令渲染或在动画循环渲染

当渲染到核心动画layer的时候,你必须选择什么时候绘制OpenGL 内容,就像使用GLKit view 和vc就要那样。
当渲染到offscreen 或者纹理,使用这个framebuff你可在任何时候绘制到任何地方。
对按照命令绘制,实现你自己的方法来绘制和展示renderbuffer,然后当你想展示新的内容的就调用这个方法。

使用动画循环绘制,要使用CADisplayLink对象。display link 是一种定时器,由核心动画提供,可以让你同步绘制和屏幕刷新速度。清单4-1显示了如何检索显示视图的屏幕,使用该屏幕创建一个新的display link对象,并将display link 添加到运行循环中。

GLKViewController 类自动使用了CADisplayLink 来动画 GLKView.只有当你执行GLKit framework 之外的操作时,你才需要直接使用CADisplayLink。

displayLink = [myView.window.screen displayLinkWithTarget:self selector:@selector(drawFrame)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

在drawFrame 实现中,读取timestamp属性来获取下一渲染帧的间隔时间。可以用来计算下一帧中object的position。

正常情况下,每次屏幕刷新的时候都会触发display link 对象;大约60Hz,但是不同的设备可能不同。大部分app 没有必要每秒60次的更新屏幕。你可以设置frameInterval属性来调节。例如,如果你设置frame interval 为3,每次第三次调用,一秒20帧。

为了获得最佳效果,选择一个帧速率你的应用程序可以始终实现。光滑的、一致的帧速率产生比帧速率的变化规律更愉快的用户体验.

渲染一帧

图片发自简书App

上图展示了,一个iOS OpenGL ES app 渲染和展示一帧 要操作的步骤。这些步骤包括很多暗示来提升app性能。

clear buffer

在每帧的开始,调用glClear方法清除,上一帧留下的内容。

glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

使用glClear 暗示着,renderbuffer当前的内容或者纹理可以被清除,避免加载以前的内容到内存中操作带来的消耗。

prepare resources and execute drawing commands 准备资源执行绘制命令

这两个步骤包含了您设计应用程序架构时所做的关键决策。首先,你决定你想要显示给用户并配置相应的OpenGL ES 对象 比如顶点缓冲区对象,纹理,上传到GPU的着色器程序的输入变量。接下来,你提交绘制命令,告诉GPU 怎么使用这些资源来渲染一帧。

渲染器设计中有更详细的介绍OpenGL ES的设计指南。现在 注意最重要的性能优化:如果只在渲染一帧开始的时候去修改OpengGL对象,你的app会运行的更快。虽然,你的app可以交替的修改OpenGL 对象和提交绘制命令,但是如果每帧每个步骤只执行一次,你的app会运行的更快。

execute drawing commands 执行绘制命令

这个步骤会使用你前面步骤准备的对象和提交绘制命令来使用这些对象。在OpenGL ES设计指南中详细地设计了渲染代码的这部分来高效运行。现在 注意最重要的性能优化:如果只在渲染一帧开始的时候去修改OpengGL对象,你的app会运行的更快。虽然,你的app可以交替的修改OpenGL 对象和提交绘制命令,但是如果每帧每个步骤只执行一次,你的app会运行的更快。

Resolve multisampling 解决多重渲染

如果你的app 使用multisampling 来提高图片质量,你的app必须在展示给用户前解决这些像素。Multisampling 详细信息看 Using Multisampling to Improve Image Quality。

discard unneeded rednerbuffer 丢弃不需要的渲染帧

一个丢弃操作是一个性能暗示告诉OpenGL 一个或者多个渲染帧的内容不再需要了。通过提示OpenGL不再需要这些渲染帧的内容了,buffer 中的数据可以丢弃了,所以一些相关的昂贵的任务可以丢弃了。
在这个阶段,你的app已经提交了全部的绘制命令。当你的app 需要颜色渲染buffer 展示到屏幕上,它可能不需要深度buffer了。
下面展示了丢弃深度buffer:

const GLenum discards[]  = {GL_DEPTH_ATTACHMENT};
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
glDiscardFramebufferEXT(GL_FRAMEBUFFER,1,discards);

注意:glDiscardFramebufferEXT函数是OpenGL ES 1.1 2.0 的EXT_discard_framebuffer扩展提供的。在3.0 中,使用glInvaliddateFramebuffer函数代替。

present the results to core animation 把结果展示到核心动画

到这一步, color renderbuffer 保持完整的帧,所有你需要做的就是展示给用户。下面展示了,绑定渲染帧到context然后展示给用户。这样就把完整的帧提交给了核心动画。

glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];

默认情况下,你必须假设 渲染内容被丢弃在您的应用程序展示后。这意味着,每次你的应用程序呈现一个frame,它必须完全重新创建frame的内容当渲染新的一帧的时候。这也是上面的代码总是清除color buffer 的原因。

如果app想保持color buffer 的内容,添加kEAGLDrawablePropertyRetainedBackingCAEAGLLayerdrawableProperties属性,并且移除 GL_COLOR_BUFFER_BIT常量。
保留的备份可能需要iOS分配额外的内存,以保存缓冲区的内容,这可能会降低你的应用程序的性能。

使用多重采样提高图片质量

多重采样是一个反锯齿方法,反锯齿可以使锯齿状边缘平滑,提高了大多数3D应用程序的图像质量。OpenGL3.0 把多重采样作为核心特性,iOS 在1.1 ,2.0 中通过APPLE_framebuffer_multisample扩展提供。多重采样使用更多的内存和处理时间片段来渲染图像,但它可以提高在一个较低的性能成本比使用其他方法的图像质量。

下图展示了多重采样如何工作。你的app 创建两个frame buffer 对象而不是一个。这个多重采样的buffer 包含所有的需要的附件来渲染内容(尤其是color 和depth buffers)。这个resolve buffer 只包含需要展示给用户的附件(尤其是color renderbuffer ,也可能是纹理),multisample renderbuffer 创建使用和resolve framebuffer 一样的尺寸,但是包含了一个参数,这个参数可以明确每个像素保存的采样个数。你的app 执行所有的渲然到这个multisamping buffer 然后产生抗锯齿的图片。

图片发自简书App

下面代码 显示了,创建多重采样的buffer。此代码使用以前创建的缓冲区的宽度和高度。它调用glrenderbufferstoragemultisampleapple函数创建多重存储的渲染。

glGenFramebuffers(1, &sampleFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, sampleFramebuffer);
 
glGenRenderbuffers(1, &sampleColorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, sampleColorRenderbuffer);
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, 4, GL_RGBA8_OES, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, sampleColorRenderbuffer);
 
glGenRenderbuffers(1, &sampleDepthRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, sampleDepthRenderbuffer);
glRenderbufferStorageMultisampleAPPLE(GL_RENDERBUFFER, 4, GL_DEPTH_COMPONENT16, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, sampleDepthRenderbuffer);
 
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    NSLog(@"Failed to make complete framebuffer object %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));

下面这些步骤修改代码支持多重采样:

  1. 在clear 步骤,清理multisampling framebuffer 的内容
glBindFramebuffer(GL_FRAMEBUFFER, sampleFramebuffer);
glViewport(0, 0, framebufferWidth, framebufferHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  1. 在提交绘制命令后,把multisamplingbuffer的内容传递到resolve buffer。每个像素对应的采样合并到一个单独的采样在resolve buffer。
glBindFramebuffer(GL_DRAW_FRAMEBUFFER_APPLE, resolveFrameBuffer);
glBindFramebuffer(GL_READ_FRAMEBUFFER_APPLE, sampleFramebuffer);
glResolveMultisampleFramebufferAPPLE();
  1. 丢弃这步。你可以丢弃multisample framebuffer的全部的renderbuffer。这是因为要展示的内容已经全部保存到resolve framebuffer中了。
const GLenum discards[]  = {GL_COLOR_ATTACHMENT0,GL_DEPTH_ATTACHMENT};
glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE,2,discards);
  1. 在展示步骤,展示resolve buffer 的color renderbuffer 附件。
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];

multisampling 不是免费的。额外的采样需要额外的内存,把采样数据整合到resolve buffer 需要时间。

推荐阅读更多精彩内容