OpenGL ES Programming Guide

OpenGL ES规范定义了一个与平台无关的API,用于使用GPU硬件呈现图形。实现OpenGL ES的平台提供了执行OpenGL ES命令的呈现上下文、保存呈现结果的framebuffer以及显示framebuffer内容的一个或多个呈现目的地。在iOS中,EAGLContext类实现了呈现上下文。iOS只提供一种framebuffer, OpenGL ES framebuffer对象,GLKView和CAEAGLLayer类实现呈现目的地。

在iOS中构建一个OpenGL ES应用程序需要考虑几个方面,其中一些是OpenGL ES编程通用的,一些是iOS专用的。按照这个清单和下面的详细部分开始:

确定OpenGL ES的哪个版本为您的应用程序设置了正确的特性集,并创建一个OpenGL ES上下文。
在运行时验证设备是否支持您想要使用的OpenGL ES功能。
选择渲染OpenGL ES内容的位置。
确保你的应用程序在iOS中正确运行。
实现您的呈现引擎。
使用Xcode和工具调试OpenGL ES应用程序,并对其进行优化,以获得最佳性能。

选择要支持的OpenGL ES版本
决定你的应用程序应该支持OpenGL ES 3.0、OpenGL ES 2.0、OpenGL ES 1.1还是多个版本。

OpenGL ES 3.0是iOS 7的新版本。它增加了许多新特性,支持更高的性能、通用的GPU计算技术,以及以前只能在桌面级硬件和游戏机上才能实现的更复杂的视觉效果。
OpenGL ES 2.0是iOS设备的基准配置文件,具有基于可编程着色器的可配置图形管道。
OpenGL ES 1.1只提供一个基本的固定功能图形管道,在iOS中主要是为了向后兼容。
你应该以支持与你的应用最相关的功能和设备的OpenGL ES版本为目标。

EAGL是OpenGL ES呈现上下文的iOS实现
在您的应用程序可以调用任何OpenGL ES函数之前,它必须初始化一个EAGLContext对象。EAGLContext类还提供了用于将OpenGL ES内容与核心动画集成的方法。

当前上下文是OpenGL ES函数调用的目标
iOS应用程序中的每个线程都有一个当前上下文;当您调用OpenGL ES函数时,这个上下文的状态会因调用而改变。

要设置线程的当前上下文,在线程上执行时调用EAGLContext类方法setCurrentContext:。

[EAGLContext setCurrentContext: myContext];

注意:如果您的应用程序在同一线程上的两个或多个上下文之间主动切换,在将新上下文设置为当前上下文之前调用glFlush函数。这确保了以前提交的命令能够及时地交付给图形硬件。

OpenGL ES持有对与当前上下文对应的EAGLContext对象的强引用。当您调用setCurrentContext:方法来更改当前上下文时,OpenGL ES不再引用前一个上下文。为了防止EAGLContext对象在非当前上下文时被释放,应用程序必须保持对这些对象的强引用(或保留)。

每个上下文都针对OpenGL ES的特定版本
EAGLContext对象只支持一个版本的OpenGL ES。例如,为OpenGL ES 1.1编写的代码与OpenGL ES 2.0或3.0上下文不兼容。使用核心OpenGL ES 2.0特性的代码与OpenGL ES 3.0上下文中兼容,为OpenGL ES 2.0扩展设计的代码通常可以在OpenGL ES 3.0上下文中使用,只需要稍加修改。许多新的OpenGL ES 3.0特性和增加的硬件功能都需要一个OpenGL ES 3.0上下文。
当OpenGL ES创建并初始化EAGLContext对象时,应用程序决定支持哪个版本。如果设备不支持所请求的OpenGL ES版本,则initWithAPI:方法返回nil。应用程序必须进行测试,以确保上下文在使用之前被成功初始化。
为了在应用程序中支持多个版本的OpenGL ES作为呈现选项,您应该首先尝试初始化您想要瞄准的最新版本的呈现上下文。如果返回的对象为nil,则初始化旧版本的上下文。

EAGLContext* CreateBestEAGLContext()
{
   EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
   if (context == nil) {
      context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
   }
   return context;
}

上下文的API属性声明该上下文支持的OpenGL版本。应用程序应该测试上下文的API属性,并使用它选择正确的呈现路径。实现这种行为的一个常见模式是为每个呈现路径创建一个类。应用程序测试上下文并在初始化时创建一个呈现器。

一个EAGL Sharegroup为上下文管理OpenGL ES对象
尽管上下文持有OpenGL ES状态,但它并不直接管理OpenGL ES对象。相反,OpenGL ES对象是由EAGLSharegroup对象创建和维护的。每个上下文都包含一个EAGLSharegroup对象,它将对象创建委托给这个对象。

当两个或多个上下文引用同一个sharegroup时,当多个上下文连接到一个公共sharegroup时,任何上下文创建的OpenGL ES对象在所有上下文中都可用时,sharegroup的优势就变得明显了;如果您在另一个上下文中绑定到相同的对象标识符,而不是创建它的上下文,那么您将引用相同的OpenGL ES对象。移动设备上的资源通常是稀缺的;在多个上下文上创建相同内容的多个副本是浪费。共享公共资源可以更好地利用设备上可用的图形资源。

sharegroup是一个不透明的对象;它没有应用程序可以调用的方法或属性。使用sharegroup对象的上下文保持对它的强引用。


image

Sharegroups在两种特定场景下最有用:

1.当上下文之间共享的大部分资源不变时。
2.当你希望你的应用程序能够在一个线程上创建新的OpenGL ES对象,而不是渲染器的主线程。在这种情况下,第二个上下文在一个单独的线程上运行,用于获取数据和创建资源。加载资源后,第一个上下文可以绑定到对象并立即使用它。GLKTextureLoader类使用这种模式提供异步纹理加载。
要创建引用同一个sharegroup的多个上下文,第一个上下文通过调用initWithAPI:初始化;将为上下文自动创建一个sharegroup。通过调用initWithAPI:sharegroup:方法,第二个上下文和后面的上下文被初始化为使用第一个上下文的sharegroup。清单2-2显示了如何工作。第一个上下文使用清单2-1中定义的便利函数创建。第二个上下文是通过从第一个上下文提取API版本和sharegroup创建的。
与同一个sharegroup关联的所有上下文都必须使用与初始上下文相同的OpenGL ES API版本。

EAGLContext* firstContext = CreateBestEAGLContext();
EAGLContext* secondContext = [[EAGLContext alloc] initWithAPI:[firstContext API] sharegroup: [firstContext sharegroup]];

当sharegroup被多个上下文共享时,应用程序有责任管理OpenGL ES对象的状态变化。规则如下:

如果对象没有被修改,应用程序可以跨多个上下文同时访问对象。
当通过发送到上下文的命令修改对象时,不能在任何其他上下文上读取或修改对象。
修改对象后,所有上下文必须重新绑定对象以查看更改。如果上下文在绑定对象之前引用了对象的内容,则对象的内容是未定义的。
以下是应用程序更新OpenGL ES对象时应该遵循的步骤:

在可能使用对象的每个上下文上调用glFlush。
在想要修改对象的上下文中,调用一个或多个OpenGL ES函数来修改对象。
在收到状态修改命令的上下文中调用glFlush。
在其他所有上下文中,重新绑定对象标识符。

共享对象的另一种方法是使用单个呈现上下文,但使用多个目标帧缓冲区。在呈现时,应用程序绑定适当的framebuffer并根据需要呈现其帧。因为所有的OpenGL ES对象都是从一个上下文中引用的,所以它们看到的是相同的OpenGL ES数据。这种模式使用的资源更少,但只适用于单线程应用程序,在这种应用程序中,您可以小心地控制上下文的状态。

使用OpenGL ES和GLKit绘图
GLKit框架提供了视图和视图控制器类,它们消除了绘制和动画OpenGL ES内容所需的设置和维护代码。GLKView类管理OpenGL ES基础设施,为您的绘图代码提供一个位置,GLKViewController类为GLKit视图中OpenGL ES内容的平滑动画提供一个呈现循环。这些类扩展了用于绘制视图内容和管理视图表示的标准UIKit设计模式。因此,您可以将主要精力集中在OpenGL ES呈现代码上,并快速启动和运行您的应用程序。GLKit框架还提供了其他功能来简化OpenGL ES 2.0和3.0的开发。

GLKit视图根据需要绘制OpenGL ES内容
GLKView类提供了一个基于OpenGL的标准UIView绘图循环。UIView实例自动配置它的图形上下文,这样你的drawRect:实现只需要执行Quartz 2D绘图命令,GLKView实例自动配置它自己,这样你的绘图方法只需要执行OpenGL ES绘图命令。GLKView类通过维护一个framebuffer对象来提供这个功能,该对象保存OpenGL ES绘制命令的结果,然后在绘制方法返回时自动将它们呈现给Core Animation。

像标准的UIKit视图一样,GLKit视图按需呈现其内容。当您的视图首次显示时,它会调用您的绘图方法—core Animation缓存呈现的输出并在视图显示时显示它。当您想要更改视图的内容时,调用它的setNeedsDisplay方法,视图再次调用绘图方法,缓存生成的图像,并将其显示在屏幕上。当用于呈现图像的数据不频繁或仅对用户操作作出响应时,这种方法非常有用。通过只在需要时呈现新视图内容,您可以节省设备上的电池电量,并为设备执行其他操作留出更多时间。


图3-1

创建和配置一个GLKit视图
您可以通过编程或使用接口构建器来创建和配置GLKView对象。在使用它进行绘图之前,必须将其与EAGLContext对象相关联(请参阅配置OpenGL ES上下文)。

以编程方式创建视图时,首先创建一个上下文,然后将其传递给视图的initWithFrame:context:方法。
从storyboard中加载视图之后,创建一个上下文并将其设置为视图的上下文属性的值。
GLKit视图自动创建和配置自己的OpenGL ES framebuffer对象和renderbuffer。您可以使用视图的可绘制属性来控制这些对象的属性。如果您更改GLKit视图的大小、缩放因子或可绘制属性,它将在下次绘制内容时自动删除并重新创建适当的framebuffer对象和renderbuffer。

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Create an OpenGL ES context and assign it to the view loaded from storyboard
    GLKView *view = (GLKView *)self.view;
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
 
    // Configure renderbuffers created by the view
    view.drawableColorFormat = GLKViewDrawableColorFormatRGBA8888;
    view.drawableDepthFormat = GLKViewDrawableDepthFormat24;
    view.drawableStencilFormat = GLKViewDrawableStencilFormat8;
 
    // Enable multisampling
    view.drawableMultisample = GLKViewDrawableMultisample4X;
}

可以使用GLKView实例的drawableMultisample属性启用多采样。多采样是一种消除锯齿边缘的反混叠方法,在大多数3D应用程序中以增加内存和碎片处理时间为代价来提高图像质量。
使用GLKit视图绘制
图3-1概述了绘制OpenGL ES内容的三个步骤:准备OpenGL ES基础架构、发出绘图命令以及将呈现的内容呈现给核心动画以供显示。GLKView类实现了第一步和第三步。对于第二步,您将实现一个绘图方法

- (void)drawRect:(CGRect)rect
{
    // Clear the framebuffer
    glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    // Draw using previously configured texture, shader, uniforms, and vertex array
    glBindTexture(GL_TEXTURE_2D, _planetTexture);
    glUseProgram(_diffuseShading);
    glUniformMatrix4fv(_uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
    glBindVertexArrayOES(_planetMesh);
    glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT);
}

glClear函数提示OpenGL ES任何现有的framebuffer内容都可以被丢弃,从而避免了昂贵的内存操作,从而将以前的内容加载到内存中。为了确保最佳性能,您应该在绘制之前调用这个函数。

GLKView类能够为OpenGL ES绘图提供一个简单的接口,因为它管理OpenGL ES渲染过程的标准部分:

在调用绘图方法之前,视图:
1.将其EAGLContext对象设置为当前上下文
2.根据当前大小、比例因子和可绘制属性(如果需要)创建framebuffer对象和renderbuffer
3.绑定framebuffer对象作为绘制命令的当前目标
4.设置OpenGL ES viewport以匹配framebuffer大小
在您的绘图方法返回后,视图:
1.解析多采样缓冲区(如果启用了多采样)
2.丢弃不再需要其内容的renderbuffer
3.将renderbuffer内容呈现给核心动画以进行缓存和显示

使用委托对象呈现
许多OpenGL ES应用程序在定制类中实现了呈现代码。这种方法的一个优点是,通过为每个呈现器定义不同的呈现器类,您可以轻松地支持多种呈现算法。共享公共功能的呈现算法可以从超类继承它。例如,您可以使用不同的呈现程序类来支持OpenGL ES 2.0和3.0(参见配置OpenGL ES上下文)。或者,您可以使用它们在具有更强大硬件的设备上定制呈现,以获得更好的图像质量。

GLKit非常适合这种方法——您可以将呈现器对象作为标准GLKView实例的委托。与子类化GLKView和实现drawRect:方法不同,renderer类采用了GLKViewDelegate协议并实现了GLKView:drawInRect:方法。演示如何在应用程序启动时基于硬件特性选择呈现器类

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    // Create a context so we can test for features
    EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:context];
 
    // Choose a rendering class based on device features
    GLint maxTextureSize;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &maxTextureSize);
    if (maxTextureSize > 2048)
        self.renderer = [[MyBigTextureRenderer alloc] initWithContext:context];
    else
        self.renderer = [[MyRenderer alloc] initWithContext:context];
 
    // Make the renderer the delegate for the view loaded from the main storyboard
    GLKView *view = (GLKView *)self.window.rootViewController.view;
    view.delegate = self.renderer;
 
    // Give the OpenGL ES context to the view so it can draw
    view.context = context;
 
    return YES;
}

一个GLKit视图控制器动画OpenGL ES内容
默认情况下,GLKView对象按需呈现其内容。也就是说,使用OpenGL ES进行绘图的一个关键优势是,它能够使用图形处理硬件对复杂场景进行连续动画——游戏和模拟等应用程序很少呈现静态图像。对于这些情况,GLKit框架提供了一个视图控制器类,为它管理的GLKView对象维护一个动画循环。这个循环遵循游戏和模拟中常见的设计模式,有两个阶段:更新和显示。图3-2显示了一个动画循环的简化示例。


图3-2

理解动画循环
对于更新阶段,视图控制器调用它自己的更新方法(或它的委托的glkViewControllerUpdate:方法)。在这种方法中,您应该准备绘制下一帧。例如,游戏可能使用这种方法来确定玩家和敌人角色的位置,基于从最后一帧接收到的输入事件,科学的可视化可能使用这种方法来运行它的模拟步骤。如果需要时间信息来确定应用程序在下一帧的状态,使用视图控制器的时间属性,比如timeSinceLastUpdate属性。在图3-2中,更新阶段增加一个角度变量,并使用它来计算转换矩阵。

对于显示阶段,视图控制器调用其视图的显示方法,该方法依次调用绘图方法。在绘图方法中,向GPU提交OpenGL ES绘图命令以呈现内容。为了获得最佳性能,您的应用程序应该在呈现新 frame开始时修改OpenGL ES对象,然后提交绘图命令。在图3-2中,显示阶段将着色程序中的统一变量设置为更新阶段计算的矩阵,然后提交一个绘图命令以呈现新内容。

动画循环以视图控制器的帧秒属性指示的速率在这两个阶段之间交替。您可以使用preferredFramesPerSecond属性来设置所需的帧速率——为了优化当前显示硬件的性能,视图控制器会自动选择一个接近您首选值的最优帧速率。

重要提示:为了获得最好的效果,选择一个你的应用能够持续达到的帧率。平滑、一致的帧速率比不规律变化的帧速率更能带来愉快的用户体验。

@implementation PlanetViewController // subclass of GLKViewController
 
- (void)viewDidLoad
{
    [super viewDidLoad];
 
    // Create an OpenGL ES context and assign it to the view loaded from storyboard
    GLKView *view = (GLKView *)self.view;
    view.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
 
    // Set animation frame rate
    self.preferredFramesPerSecond = 60;
 
    // Not shown: load shaders, textures and vertex arrays, set up projection matrix
    [self setupGL];
}
 
- (void)update
{
    _rotation += self.timeSinceLastUpdate * M_PI_2; // one quarter rotation per second
 
    // Set up transform matrices for the rotating planet
    GLKMatrix4 modelViewMatrix = GLKMatrix4MakeRotation(_rotation, 0.0f, 1.0f, 0.0f);
    _normalMatrix = GLKMatrix3InvertAndTranspose(GLKMatrix4GetMatrix3(modelViewMatrix), NULL);
    _modelViewProjectionMatrix = GLKMatrix4Multiply(_projectionMatrix, modelViewMatrix);
}
 
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    // Clear the framebuffer
    glClearColor(0.0f, 0.0f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 
    // Set shader uniforms to values calculated in -update
    glUseProgram(_diffuseShading);
    glUniformMatrix4fv(_uniformModelViewProjectionMatrix, 1, 0, _modelViewProjectionMatrix.m);
    glUniformMatrix3fv(_uniformNormalMatrix, 1, 0, _normalMatrix.m);
 
    // Draw using previously configured texture and vertex array
    glBindTexture(GL_TEXTURE_2D, _planetTexture);
    glBindVertexArrayOES(_planetMesh);
    glDrawElements(GL_TRIANGLE_STRIP, 256, GL_UNSIGNED_SHORT, 0);
}
 
@end

在本例中,一个PlanetViewController类(一个自定义GLKViewController子类)的实例从一个故事板加载,以及一个标准的GLKView实例及其可绘制属性。viewDidLoad方法创建一个OpenGL ES上下文并将其提供给视图,还设置动画循环的帧速率。

视图控制器是其视图的自动委托,因此它实现了动画循环的更新和显示阶段。在update方法中,它计算显示旋转行星所需的转换矩阵。在glkView:drawInRect:方法中,它向着色程序提供这些矩阵,并提交绘制命令来呈现行星的几何形状。

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


图4-1

所有这些方法都需要手动创建framebuffer和renderbuffer对象来存储OpenGL ES上下文的呈现结果,并编写额外的代码将其内容显示到屏幕上(如果需要的话)并运行动画循环。

创建Framebuffer对象
根据应用程序要执行的任务,应用程序配置不同的对象以附加到framebuffer对象。在大多数情况下,配置framebuffer的区别在于哪个对象附加到framebuffer对象的颜色附着点上:
1.要使用framebuffer进行屏幕外图像处理,请附加一个renderbuffer。
2.要将framebuffer图像用作稍后呈现步骤的输入,请附加一个纹理。
3.要在核心动画层合成中使用framebuffer,请使用特殊的支持核心动画的renderbuffer。
创建屏幕外的Framebuffer对象
一个用于屏幕外渲染的framebuffer将其所有附件分配为OpenGL ES渲染缓冲区。下面的代码分配了一个带有颜色和深度附件的framebuffer对象。
1.创建framebuffer并将其绑定。

GLuint framebuffer;
glGenFramebuffers(1, &framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);

2.创建一个颜色渲染缓冲区,为其分配存储空间,并将其附加到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);

3.创建深度或深度/模板渲染缓冲区,为其分配存储空间,并将其附加到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);

4.测试 frame缓冲区的完整性。这个测试只需要在framebuffer的配置发生变化时执行。

GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER) ;
if(status != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"failed to make complete framebuffer object %x", status);
}

绘制到屏幕外的renderbuffer后,您可以使用glReadPixels将其内容返回给CPU进行进一步处理。

使用Framebuffer对象渲染纹理
创建这个framebuffer的代码几乎与屏幕外的示例相同,但是现在分配了一个纹理并将其附加到颜色附件点。

1.创建framebuffer对象。
2.创建目标纹理,并将其附加到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);

3.分配和附加一个深度缓冲区。
4.测试framebuffer的完整性。
虽然本例假设您正在渲染一个颜色纹理,但其他选项是可能的。例如,使用OES_depth_texture扩展,您可以将纹理附加到深度附件点,以将场景中的深度信息存储到纹理中。您可以使用这个深度信息来计算最终渲染场景中的阴影。

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


image

CAEAGLLayer通过提供两个关键的功能部件向OpenGL ES提供了这种支持。首先,它为renderbuffer分配共享存储。其次,它将renderbuffer呈现给Core Animation,用renderbuffer的数据替换层之前的内容。这种模型的一个优点是,核心动画层的内容不需要在每一帧中绘制,只有当渲染的图像发生变化时。

注意:GLKView类自动化了下面的步骤,所以当您想在视图的内容层中使用OpenGL ES绘制时,应该使用它。

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

1.创建CAEAGLLayer对象并配置其属性。
为了获得最佳性能,将图层的不透明属性设置为YES。请注意核心动画合成性能。
可以选择,通过为CAEAGLLayer对象的drawableProperties属性分配一个新的值字典来配置呈现表面的表面属性。您可以指定renderbuffer的像素格式,并指定renderbuffer的内容是否在发送到Core Animation后被丢弃。有关允许的键的列表,请参阅EAGLDrawable协议引用。
2.分配一个OpenGL ES上下文并使其成为当前上下文。
3.创建framebuffer对象。
4.创建一个颜色渲染缓冲区,通过调用上下文的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的存储空间。如果不重新分配renderbuffer, renderbuffer大小将不匹配层的大小;在这种情况下,核心动画可以缩放图像的内容以适应图层。
5.检索颜色渲染缓冲区的高度和宽度。

GLint width;
GLint height;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &width);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &height);

在前面的示例中,显式地提供了renderbuffer的宽度和高度,以分配缓冲区的存储空间。在这里,代码在分配了color renderbuffer的存储之后从它检索宽度和高度。你的应用程序这样做是因为颜色渲染缓冲区的实际尺寸是根据层的边界和比例系数计算的。附加到framebuffer上的其他renderbuffer必须具有相同的维数。除了使用高度和宽度来分配深度缓冲区外,还可以使用它们来分配OpenGL ES视口,并帮助确定应用程序的纹理和模型所需的详细级别。参见支持高分辨率显示。
6.分配和附加一个深度缓冲区。
7.测试framebuffer的完整性。
8.通过将CAEAGLLayer对象传递给addsubblayer:可见层的方法,添加到核心动画层层次结构中。

绘制到Framebuffer对象
现在有了framebuffer对象,需要填充它。本节描述呈现新 frame并将其呈现给用户所需的步骤。对纹理或屏幕外framebuffer的呈现方式类似,只是应用程序使用最终帧的方式不同。

按需呈现或带有动画循环
当渲染到核心动画层时,你必须选择何时绘制OpenGL ES内容,就像使用GLKit视图和视图控制器绘制时一样。如果渲染到屏幕外的framebuffer或纹理,只要适合使用这些类型的framebuffer,就绘制。
对于按需绘制,实现您自己的方法来绘制和显示renderbuffer,并在需要显示新内容时调用它。
要使用动画循环绘制,使用CADisplayLink对象。显示链接是Core Animation提供的一种定时器,可以让你同步绘图到屏幕的刷新率。展示了如何检索显示视图的屏幕,使用该屏幕创建一个新的显示链接对象,并将显示链接对象添加到运行循环中。
注意:GLKViewController类自动使用CADisplayLink对象来动画GLKView内容。只有当您需要超出GLKit框架所提供的行为时,才可以直接使用CADisplayLink类。

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

在drawFrame方法的实现中,读取display链接的timestamp属性以获得下一个要呈现的帧的时间戳。它可以使用该值来计算下一帧中对象的位置。
通常,每次刷新屏幕时都会触发显示链接对象;该值通常为60hz,但在不同设备上可能有所不同。大多数应用程序不需要每秒更新60次屏幕。您可以将display链接的frameInterval属性设置为调用方法之前经过的实际帧数。例如,如果帧间隔设置为3,你的应用程序每三帧调用一次,或者大约每秒20帧。

重要提示:为了获得最好的效果,选择一个你的应用能够持续达到的帧率。平滑、一致的帧速率比不规律变化的帧速率更能带来愉快的用户体验。

呈现一个 frame
图4-3显示了OpenGL ES应用程序在iOS上呈现和呈现 frame时应该采取的步骤。这些步骤包含了许多改善应用程序性能的提示。


图4-3

清除缓冲区
在每一帧开始时,删除所有framebuffer附件的内容,这些附件的内容不需要从上一帧中获取,就可以绘制下一帧。调用glClear函数,传入一个带有所有缓冲区的位掩码,以清除所有缓冲区

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

使用glClear“提示”来OpenGL ES渲染缓冲区或纹理的现有内容可以被丢弃,从而避免了将以前的内容加载到内存中的代价高昂的操作。

准备资源并执行绘图命令
这两个步骤包含了您在设计应用程序架构时所做的大多数关键决策。首先,您决定要向用户显示什么,并配置相应的OpenGL ES对象(如顶点缓冲区对象、纹理、着色程序及其输入变量),以便将其上传到GPU。接下来,您将提交指示GPU如何使用这些资源来呈现 frame的绘图突击。
在OpenGL ES设计指南中详细介绍了呈现器设计。目前,需要注意的最重要的性能优化是,如果您的应用程序只在呈现新 frame开始时修改OpenGL ES对象,那么它的运行速度会更快。虽然您的应用程序可以在修改对象和提交绘图命令(如图4-3中虚线所示)之间切换,但是如果每一帧只执行一个步骤,那么它的运行速度会更快。

执行绘图命令
这个步骤获取您在前一个步骤中准备的对象,并提交绘图命令来使用它们。在OpenGL ES设计指南中详细介绍了如何设计高效运行的渲染代码的这一部分。目前,需要注意的最重要的性能优化是,如果您的应用程序只在呈现新 frame时修改OpenGL ES对象,那么它的运行速度会更快。虽然您的应用程序可以在修改对象和提交绘图命令(如虚线所示)之间切换,但是如果它只执行每一步一次,那么运行速度就会更快。

解决Multisampling
如果你的应用程序使用多采样来提高图像质量,你的应用程序必须解决像素之前,他们呈现给用户。在使用多采样来提高图像质量时,详细介绍了多采样技术。

丢弃不需要的Renderbuffers
丢弃操作是一个性能提示,它告诉OpenGL ES不再需要一个或多个renderbuffer的内容。通过向OpenGL ES暗示您不需要renderbuffer的内容,缓冲区中的数据可以被丢弃,并且可以避免昂贵的任务来更新那些缓冲区的内容。
在呈现循环的这个阶段,您的应用程序已经为 frame提交了所有的绘图命令。虽然你的应用程序需要颜色渲染缓冲来显示在屏幕上,但它可能不需要深度缓冲的内容。下面展示丢弃深度缓冲区的内容。

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

glDiscardFramebufferEXT函数由EXT_discard_framebuffer扩展提供,适用于OpenGL ES 1.1和2.0。在OpenGL ES 3.0上下文中,使用glInvalidateFramebuffer函数。

将结果呈现给Core Animation
在这个步骤中,color renderbuffer保存完成的帧,所以您需要做的就是将它呈现给用户。将renderbuffer绑定到上下文并呈现它。这使得完成的帧被交给核心动画。

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

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

如果您的应用程序希望在帧之间保留颜色渲染缓冲区的内容,那么将keagldrawablepropertyretainedbackkey添加到存储在CAEAGLLayer对象的drawableProperties属性中的字典中,并从前面的glClear函数调用中删除GL_COLOR_BUFFER_BIT常量。保留备份可能需要iOS分配额外的内存来保存缓冲区的内容,这可能会降低应用程序的性能。

使用多采样来提高图像质量
在大多数3D应用程序中,多重采样是一种消除锯齿边缘和提高图像质量的反走样方法。OpenGL ES 3.0将多采样作为核心规范的一部分,iOS通过APPLE_framebuffer_multisample扩展在OpenGL ES 1.1和2.0中提供了多采样。多采样使用更多的内存和片段处理时间来呈现图像,但与其他方法相比,它可能以较低的性能成本提高图像质量。
图4-4显示了多采样的工作原理。不是创建一个framebuffer对象,而是创建两个。多采样缓冲区包含呈现内容所需的所有附件(通常是颜色和深度缓冲区)。解析缓冲区只包含显示呈现给用户的图像所需的附件(通常是彩色呈现缓冲区,但可能是纹理),这些附件是通过创建Framebuffer对象的适当过程创建的。多示例renderbuffer的分配使用与解析framebuffer相同的维数,但每个都包含一个额外的参数,该参数指定每个像素要存储的样本数量。您的应用程序将其全部呈现到多采样缓冲区,然后通过将这些示例解析到解析缓冲区来生成最终的抗锯齿图像。使用多采样来提高图像质量。


图4-4

显示创建多采样缓冲区的代码。这段代码使用先前创建的缓冲区的宽度和高度。它调用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));

以下是修改呈现代码以支持多采样的步骤:
1.在清除缓冲区的步骤中,清除多采样帧缓冲区的内容。

glBindFramebuffer(GL_FRAMEBUFFER, sampleFramebuffer);
glViewport(0, 0, framebufferWidth, framebufferHeight);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

2.提交绘图命令后,将从多采样缓冲区解析内容到解析缓冲区。为每个像素存储的样本合并到解析缓冲区中的单个样本中。

glBindFramebuffer(GL_DRAW_FRAMEBUFFER_APPLE, resolveFrameBuffer);
glBindFramebuffer(GL_READ_FRAMEBUFFER_APPLE, sampleFramebuffer);
glResolveMultisampleFramebufferAPPLE();

3.在释放步骤中,您可以释放附加到多示例framebuffer的renderbuffer。这是因为您计划呈现的内容存储在解析framebuffer中。

const GLenum discards[]  = {GL_COLOR_ATTACHMENT0,GL_DEPTH_ATTACHMENT};
glDiscardFramebufferEXT(GL_READ_FRAMEBUFFER_APPLE,2,discards);

4.在当前结果步骤中,您将显示连接到解析帧缓冲区的颜色呈现缓冲区。

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

多采样不是免费的;需要额外的内存来存储额外的样例,而将样例解析到resolve framebuffer需要时间。如果你在你的应用程序中加入了多采样,总是要测试你的应用程序的性能,以确保它仍然是可接受的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容

  • 悠久的中华文化,祖先们追求长生不老的故事以及为求长生不老而流传下来的各种功法,在世间流传。 成仙,成神,被历代人们...
    凡非阅读 501评论 0 4
  • 偷个cookie 生效不?
    whoami_000阅读 226评论 1 0
  • 樊登读书会员已经100多天了,其中也断断续续听了一些书,但听了也只是听过了而已,过后,就会忘记,这本书讲到了什么,...
    依依妈_5211阅读 686评论 0 6
  • 说声谢谢道声珍重人生远航中不再回头人生远航中不能回头明白了许多将来还会明白更多
    李一十八阅读 62评论 0 1