OpenGL ES 框架详细解析(六) —— 绘制到其他渲染目的地

版本记录

版本号 时间
V1.0 2017.09.29

前言

OpenGL ES是一个强大的图形库,是跨平台的图形API,属于OpenGL的一个简化版本。iOS系统可以利用OpenGL ES将图像数据直接送入到GPU进行渲染,这样避免了从CPU进行计算再送到显卡渲染带来的性能的高消耗,能带来来更好的视频效果和用户体验。接下来几篇就介绍下iOS 系统的 OpenGL ES框架。感兴趣的可以看上面几篇。
1. OpenGL ES 框架详细解析(一) —— 基本概览
2. OpenGL ES 框架详细解析(二) —— 关于OpenGL ES
3. OpenGL ES 框架详细解析(三) —— 构建用于iOS的OpenGL ES应用程序的清单
4. OpenGL ES 框架详细解析(四) —— 配置OpenGL ES的上下文
5. OpenGL ES 框架详细解析(五) —— 使用OpenGL ES和GLKit进行绘制

绘制到其他渲染目的地

Framebuffer对象是渲染命令的目标。 当您创建一个framebuffer对象时,您可以对其存储的颜色,深度和模板数据进行精确的控制。 您可以通过将图像附加到帧缓冲区来提供此存储空间,下图所示。 最常见的图像附件是一个renderbuffer对象。 您还可以将OpenGL ES纹理附加到帧缓冲区的颜色附加点,这意味着任何绘图命令都将呈现到纹理中。 后来,纹理可以作为未来渲染命令的输入。 您还可以在单个渲染上下文中创建多个帧缓冲区对象。 您可以这样做,以便在多个帧缓冲区之间共享相同的渲染管道和OpenGL ES资源。

Framebuffer with color and depth renderbuffers

所有这些方法都需要手动创建framebufferrenderbuffer对象来存储来自OpenGL ES上下文的渲染结果,以及编写其他代码以将其内容呈现给屏幕,(如果需要)运行动画循环。


Creating a Framebuffer Object - 创建Framebuffer对象

根据您的应用程序要执行的任务,您的应用程序将配置不同的对象以附加到framebuffer对象。 在大多数情况下,配置帧缓冲区的区别在于哪个对象附加到framebuffer对象的颜色附加点上:

1. Creating Offscreen Framebuffer Objects - 创建离屏帧缓冲对象

用于屏幕外渲染的帧缓冲区将其所有附件分配为OpenGL ES渲染缓冲区。 以下代码分配带有颜色和深度附件的帧缓冲区对象。

  • 创建帧缓冲区并绑定它。
GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
  • 创建一个颜色renderbuffer,为它分配存储,并将其附加到framebuffer的颜色附加点。
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);
  • 创建深度或深度/模板renderbuffer,为其分配存储空间,并将其附加到framebuffer的深度附件点。
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);
  • 测试帧缓冲区的完整性。 只有当帧缓冲区的配置更改时,才需要执行此测试。
GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER) ;
if(status != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"failed to make complete framebuffer object %x", status);
}

在绘制到屏幕外渲染缓冲区之后,您可以将其内容返回到CPU,以使用glReadPixels函数进行进一步处理。

2. Using Framebuffer Objects to Render to a Texture - 使用Framebuffer对象渲染到纹理

创建此帧缓冲区的代码与屏幕外的示例几乎相同,但是现在将分配纹理并附加到颜色附加点。

// 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);
  • 分配并附加深度缓冲区(如前所述)。
  • 测试帧缓冲区的完整性(如前所述)。

尽管此示例假定您要呈现为颜色纹理,但其他选项也是可能的。 例如,使用OES_depth_texture扩展名,您可以将纹理附加到深度附件点,以将场景中的深度信息存储到纹理中。 您可以使用此深度信息来计算最终渲染场景中的阴影。

3. Rendering to a Core Animation Layer - 渲染到Core Animation层

核心动画是iOS上图形渲染和动画的核心基础。 您可以使用主持使用不同iOS子系统(如UIKit,Quartz 2D和OpenGL ES)呈现的内容的图层合成应用程序的用户界面或其他视觉显示。 OpenGL ES通过CAEAGLLayer类连接到Core Animation,这是一种特殊类型的Core Animation图层,其内容来自OpenGL ES renderbuffer。 Core Animation将renderbuffer的内容与其他图层复合,并在屏幕上显示生成的图像。

CAEAGLLayer通过提供两个关键功能向OpenGL ES提供此支持。 首先,它为renderbuffer分配共享存储。 其次,它将渲染缓冲区呈现给Core Animation,用renderbuffer中的数据替换了以前的内容。 该模型的一个优点是,只有当渲染的图像更改时,Core Animation图层的内容不需要在每个帧中绘制。

注意:GLKView类可以自动执行以下步骤,因此当您要在视图的内容层中绘制OpenGL ES时,应使用它。

为OpenGL ES渲染使用核心动画层:

可选地,通过为CAEAGLLayer对象的drawableProperties属性分配一个新的值字典来配置渲染表面的表面属性。 您可以指定renderbuffer的像素格式,并指定renderbuffer的内容在被发送到Core Animation之后是否被丢弃。 有关允许密钥的列表,请参阅EAGLDrawable Protocol Reference

  • 分配OpenGL ES上下文并使其成为当前上下文。 请参阅Configuring OpenGL ES Contexts
  • 创建framebuffer对象(如上面的 Creating Offscreen Framebuffer Objects )。
  • 创建一个颜色renderbuffer,通过调用上下文的renderbufferStorage:fromDrawable:方法,分配其存储并传递层对象作为参数。 宽度,高度和像素格式取自层,用于为renderbuffer分配存储空间。
GLuint colorRenderbuffer;
glGenRenderbuffers(1, &colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[myContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:myEAGLLayer];
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, colorRenderbuffer);

注意:当核心动画层的边界或属性更改时,应用程序应重新分配renderbuffer的存储空间。 如果不重新分配renderbuffers,renderbuffer大小将不匹配图层的大小;在这种情况下,Core Animation可以缩放图像的内容以适应图层。

  • 检索颜色renderbuffer的高度和宽度。
GLint width;
GLint height;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);

在前面的示例中,显式提供了renderbuffers的宽度和高度来为缓冲区分配存储空间。 这里,代码在分配存储后从颜色renderbuffer中检索宽度和高度。 您的应用程序执行此操作是因为颜色renderbuffer的实际尺寸是基于图层的边界和比例因子计算的。 附加到帧缓冲区的其他渲染缓冲区必须具有相同的尺寸。 除了使用高度和宽度来分配深度缓冲区之外,还可以使用它们来分配OpenGL ES视口,并帮助确定应用程序纹理和模型所需的详细程度。 请参阅Supporting High-Resolution Displays

  • 分配并附加深度缓冲区(如前所述)。
  • 测试帧缓冲区的完整性(如前所述)。
  • CAEAGLLayer对象添加到Core Animation层次结构,将其传递给可见层的addSublayer:方法。

Drawing to a Framebuffer Object - 绘制到帧缓存区对象

现在你有一个framebuffer对象,你需要填写它。 本节介绍渲染新帧并将其呈现给用户所需的步骤。 渲染到纹理或屏幕外帧缓冲区的作用类似,仅在应用程序使用最终帧时有所不同。

1. Rendering on Demand or with an Animation Loop - 按需渲染或动画循环

当渲染到Core Animation图层时,您必须选择何时绘制OpenGL ES内容,就像使用GLKit视图和视图控制器进行绘制时一样。 如果渲染到屏幕外的帧缓冲区或纹理,每当适用于使用这些帧缓冲区的情况时进行绘制。

对于按需绘图,实现您自己的方法来绘制和呈现您的renderbuffer,并在您想要显示新内容时调用它。

要使用动画循环绘制,请使用CADisplayLink对象。 一个display link对象是Core Animation提供的一种定时器,可让您将绘图同步到画面的刷新率。 下面代码显示了如何检索显示视图的屏幕,使用该屏幕创建新的display link对象,并将display link对象添加到运行循环。

注意:GLKViewController类可自动使用CADisplayLink对象来动画化GLKView内容。 仅当您需要超出GLKit框架提供的行为时才直接使用CADisplayLink类。

// Creating and starting a display link

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

drawFrame方法的实现之中,读取display linktimestamp属性以获取要渲染的下一个帧的时间戳。 它可以使用该值来计算下一帧中的对象的位置。

通常,每次屏幕刷新时触发display link,该值通常为60 Hz,但在不同的设备上可能会有所不同。 大多数应用程序不需要每秒更新屏幕60次。 您可以将display linkframeInterval属性设置为在调用方法之前执行的实际帧数。 例如,如果帧间隔设置为3,则您的应用程序称为每第三帧,或大约每秒20帧。

重要提示:为获得最佳效果,请选择app可以连续的帧率,一种平滑、一致的帧率产生比不定期变化的帧速率更愉快的用户体验。

2. Rendering a Frame - 渲染一帧

下图显示了OpenGL ES应用程序在iOS上渲染和展示帧要执行的步骤,这些步骤包括提高应用程序性能的许多提示。

iOS OpenGL Rendering Steps

3. Clear Buffers - 清除缓冲

在每帧开始时,擦除所有帧缓冲附件的内容,其中不需要前一帧的内容来绘制下一帧。 调用glClear函数,将带有位掩码的所有缓冲区进行清除,如下面代码所示。

// Clear framebuffer attachments

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

对OpenGL ES使用glClear“提示”可以丢弃renderbuffer或纹理的现有内容,避免将以前的内容加载到内存中进行的昂贵的操作。

4. Prepare Resources and Execute Drawing Commands - 准备资源和执行绘制命令

这两个步骤包括您在设计应用程序架构时所做的大多数关键决策。 首先,您决定要向用户显示什么,并配置相应的OpenGL ES对象(如顶点缓冲区对象,纹理,着色器程序及其输入变量)以上传到GPU。 接下来,您提交绘图指令,告诉GPU如何使用这些资源来渲染帧。

OpenGL ES Design Guidelines中更详细地介绍了渲染器设计。 现在,要注意的最重要的性能优化是,只有在渲染新帧的开始时,您的应用程序才能更快地修改OpenGL ES对象。 虽然您的应用程序可以在修改对象和提交绘图命令(如上图中的虚线所示)之间交替,但如果每帧仅执行一个步骤,则其运行速度更快。

5. Execute Drawing Commands - 执行绘图命令

此步骤将使用您在上一步中准备的对象,并提交绘图命令以使用它们。 在OpenGL ES Design Guidelines中详细介绍了如何将此部分渲染代码设计为高效运行。 现在,要注意的最重要的性能优化是,如果在开始渲染新帧时仅修改OpenGL ES对象,则应用程序运行速度更快。 虽然您的应用程序可以在修改对象和提交绘图命令(如虚线所示)之间交替,但如果它只执行一次,则运行速度更快。

6. Resolve Multisampling - 解决多重采样

如果您的应用程序使用多重采样来提高图像质量,则应用程序必须在呈现给用户之前解析像素。 多采样在Using Multisampling to Improve Image Quality中有详细的介绍。

7. Discard Unneeded Renderbuffers - 丢弃不需要的Renderbuffers

丢弃操作是一个性能提示,它告诉OpenGL ES,不再需要一个或多个渲染缓冲区的内容。 通过暗示OpenGL ES,您不需要renderbuffer的内容,缓冲区中的数据可以被丢弃,并且可以避免更新这些缓冲区内容的昂贵任务。

在渲染循环的这个阶段,您的应用程序已经提交了帧所有绘图命令。 当您的应用程序需要彩色渲染缓冲区才能显示到屏幕上时,它可能不需要深度缓冲区的内容。 下面代码丢弃了深度缓冲区的内容。

// Discarding the depth framebuffer

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扩展提供。 在OpenGL ES 3.0上下文中,使用glInvalidateFramebuffer函数。

8. 将结果呈现给核心动画

在此步骤中,颜色renderbuffer保存完成的帧,所以您需要做的就是将其呈现给用户。 下面代码将renderbuffer绑定到上下文并呈现它。 这将导致完成的帧被交给核心动画。

// Presenting the finished frame

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

默认情况下,您必须假定在应用程序呈现renderbuffer后,renderbuffer的内容将被丢弃。 这意味着,每当您的应用程序呈现帧时,它必须在渲染新帧时完全重新创建帧的内容。 由于这个原因,上面的代码总是擦除颜色缓冲区。

如果您的应用程序想要保留帧之间的颜色renderbuffer的内容,请将kEAGLDrawablePropertyRetainedBacking密钥添加到CAEAGLLayer对象的drawableProperties属性中存储的字典中,并从较早的glClear函数调用中删除GL_COLOR_BUFFER_BIT常量。 保留的背景可能需要iOS才能分配额外的内存来保留缓冲区的内容,这可能会降低应用程序的性能。


Using Multisampling to Improve Image Quality - 使用多重采样来提高图像质量

多采样是一种抗锯齿形式,可以在大多数3D应用程序中平滑锯齿状边缘并提高图像质量。 OpenGL ES 3.0包括多采样作为核心规范的一部分,iOS通过APPLE_framebuffer_multisample扩展在OpenGL ES 1.1和2.0中提供。 多采样使用更多的内存和片段处理时间来渲染图像,但它可以以比使用其他方法更低的性能成本来提高图像质量。

下图显示了多重采样的工作原理。 而不是创建一个帧缓冲区对象,您的应用程序将创建两个。 多重采样缓冲区包含渲染内容所需的所有附件(通常为彩色和深度缓冲区)。 解析缓冲区仅包含向用户显示渲染图像所必需的附件(通常为彩色渲染缓冲区,但可能是纹理),使用“创建帧缓冲区对象”中的相应过程创建。 多重采样渲染缓冲区使用与解析帧缓冲区相同的维度进行分配,但每个都包含一个附加参数,该参数指定为每个像素存储的采样数。 您的应用程序将其所有渲染执行到多重采样缓冲区,然后通过将这些样本解析为解析缓冲区来生成最终的抗锯齿图像。

How multisampling works

下面代码显示了创建多采样缓冲区的代码。 此代码使用先前创建的缓冲区的宽度和高度。 它调用glRenderbufferStorageMultisampleAPPLE函数为renderbuffer创建多采样存储。

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));

以下是修改渲染代码以支持多采样的步骤:

  • 在清除缓冲区步骤中,清除多重采样帧缓冲区的内容。
glBindFramebuffer(GL_FRAMEBUFFER, sampleFramebuffer);
glViewport(0, 0, framebufferWidth, framebufferHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  • 提交绘图命令后,可将多重采样缓冲区中的内容解析为解析缓冲区。 为每个像素存储的样本被合并到解析缓冲区中的单个样本中。
glBindFramebuffer(GL_DRAW_FRAMEBUFFER_APPLE, resolveFrameBuffer);
glBindFramebuffer(GL_READ_FRAMEBUFFER_APPLE, sampleFramebuffer);
glResolveMultisampleFramebufferAPPLE();
  • 在丢弃步骤中,您可以丢弃附加到多重采样帧缓冲区的两个renderbuffer。 这是因为您计划呈现的内容存储在解析帧缓冲区中。
const GLenum discards[]  = {GL_COLOR_ATTACHMENT0,GL_DEPTH_ATTACHMENT};
glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE,2,discards);
  • 在呈现结果步骤中,您将呈现附加到解析帧缓冲区的颜色renderbuffer。
glBindRenderbuffer(GL_RENDERBUFFER, colorRenderbuffer);
[context presentRenderbuffer:GL_RENDERBUFFER];

多次采样不是免费的,需要额外的内存来存储附加样本,并将样本解析为解析帧缓冲区需要时间。 如果您向应用程序添加多重采样,请始终测试应用程序的性能,以确保其仍然可以接受。

注意:上述代码假定为OpenGL ES 1.12.0上下文。 多采样是OpenGL ES 3.0 API核心的一部分,但函数不同。 详见规范。

后记

未完,待续~~~~

推荐阅读更多精彩内容