OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点

本文档继续讨论上一篇文档OpenGL ES 3.0 数据可视化 1:绘制圆点存在的锯齿问题,尝试使用多重采样(Multisampling)进行抗锯齿并期望得到光滑圆点,首先介绍基于OpenGL ES 1.0 GL_POINT_SMOOTH的实现,接着详细描述OpenGL ES 3.0实现多重采样的步骤。完整代码托管在GitHub: MultisamplingRoundPoint。

1、OpenGL ES 1.0绘制光滑圆点

原书代码基于glfw框架无法直接在iOS上运行,简单起见,现使用OpenGL ES 1.0接口作朴素实现。仔细观察最右边的圆点,可见OpenGL ES 1.0通过指定GL_POINT_SMOOTH及混合(blend)的实现在iPad Air 2上运行依然存在锯齿。

ES 1.0 GL_POINT_SMOOTH效果

关键代码如下所示。

typedef struct {
    GLfloat x, y, z; //position
    GLfloat r, g, b, a; //color and alpha channels
} Vertex;

void drawPoint(Vertex v1, GLfloat size) {
    glPushMatrix();
        glPointSize(size);

        glEnableClientState(GL_VERTEX_ARRAY);
        glEnableClientState(GL_COLOR_ARRAY);

        glVertexPointer(3, GL_FLOAT, sizeof(Vertex), &v1);
        glColorPointer(4, GL_FLOAT, sizeof(Vertex), &v1.r);

        glDrawArrays(GL_POINTS, 0, 1);

        glDisableClientState(GL_VERTEX_ARRAY);
        glDisableClientState(GL_COLOR_ARRAY);

    glPopMatrix();
}

void drawPointsDemo(int width, int height) {
    GLfloat size = 30.0f;
    for (GLfloat x = -.9f; x <= 1.0f; x += 0.15f, size += 10) {
        Vertex v1 = {x, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f};
        drawPoint(v1, size);
    }
}

- (void)layoutSubviews {
    // ...省略配置EAGLContext代码
    GLuint renderbuffer;
    glGenRenderbuffersOES(1, &renderbuffer);
    glBindRenderbufferOES(GL_RENDERBUFFER_OES, renderbuffer);
    [context renderbufferStorage:GL_RENDERBUFFER_OES fromDrawable:layer];

    GLint width, height;
    glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, 
        GL_RENDERBUFFER_WIDTH_OES, 
        &width);
    glGetRenderbufferParameterivOES(GL_RENDERBUFFER_OES, 
        GL_RENDERBUFFER_HEIGHT_OES, 
        &height);

    GLuint framebuffer;
    glGenFramebuffersOES(1, &framebuffer);
    glBindFramebufferOES(GL_FRAMEBUFFER_OES, framebuffer);
    glFramebufferRenderbufferOES(GL_FRAMEBUFFER_OES, 
        GL_COLOR_ATTACHMENT0_OES, 
        GL_RENDERBUFFER_OES, 
        renderbuffer);

    glEnable(GL_POINT_SMOOTH);
    glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, 
        GL_ONE_MINUS_SRC_ALPHA);

    glViewport(0, 0, width, height);
    glClear(GL_COLOR_BUFFER_BIT);

    drawPointsDemo(width, height);

    [context presentRenderbuffer:GL_RENDERBUFFER_OES];
}

@end

Starting with the iPhone 3GS, newer devices are equipped with the SGX series of GPUs. The SGX series features support for the OpenGL ES2.0 and newer devices support the OpenGL ES3.0 rendering API and vertex and pixel shaders. The Fixed-function pipeline is not supported natively on such GPUs, but instead is emulated by generating vertex and pixel shaders with analogous functionality on the fly.
摘自Unit手册Unity - Manual: iOS Hardware Guide

同时,根据WWDC讲座,也侧面说明了在比如iPhone 6这些现代iPhone上,OpenGL ES 1.0这种固定功能渲染管线已无硬件支持,相反,当开发者运行OpenGL ES 1.0代码时,通过可编程渲染管线模拟老版本图形管线的行为,比如,开发者调用glEnable(GL_LIGHT),OpenGL ES驱动为之生成功能相同的着色器代码,并缓存到一个哈希表中,在glDraw系列函数调用时才开始执行这些着色器代码,如下两图所示。

OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点_第1张图片
固定功能函数全由着色器模拟实现
OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点_第2张图片
固定功能函数生成的着色器代码

那么,提示OpenGL ES 1.0绘制光滑圆点的GL_POINT_SMOOTH代码很可能对应于OpenGL ES 3.0的实现就是Coverage多重采样。

在iPhone 7已到手的年代,还是让我们使用更现代的接口实现如下功能吧。

glEnable(GL_POINT_SMOOTH);
glHint(GL_POINT_SMOOTH_HINT, GL_NICEST);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, 
    GL_ONE_MINUS_SRC_ALPHA);

2、OpenGL ES 3.0实现多重采样抗锯齿

在OpenGL ES 3.0中实现多重采样的编程类似于OpenGL ES 2.0,简要描述如下所示。区别是以前是苹果等厂家自行添加的拓展函数,如今纳入标准接口,故出现部分函数去除OES、APPLE等后缀,甚至完全改名。

  1. 配置单采样(Single sampled)渲染及帧缓冲区
  2. 配置多重采样(Multisampled)渲染及帧缓冲区
  3. 绑定多重采样渲染及帧缓冲区
  4. 设置视口为单采样渲染缓冲区大小
  5. 绘制
  6. 绑定多重采样帧缓冲区为读缓冲区GL_READ_FRAMEBUFFER
  7. 绑定单采样帧缓冲区为绘制缓冲区GL_DRAW_FRAMEBUFFER
  8. 绑定单采样渲染缓冲区
  9. 交换前后帧缓冲区

在开始修改代码前,先查询OpenGL ES 3.0支持的多重采样倍数,观察其最高数值,避免超出其支持范围,导致报错。

GLint maxSupportSamples;
glGetIntegerv(GL_MAX_SAMPLES, &maxSupportSamples);
printf("max support samples = %d\n", maxSupportSamples);

如上代码在iPad Air 2(iOS 9.3.4)上得到4倍。那么,接下来的实现直接使用4倍,查看最大采样倍数下对生成圆点的影响。

2.1、细述编程步骤

1、配置单采样(Single sampled)渲染及帧缓冲区
和前面开发的常规OpenGL ES程序一样,先配置普通采样的渲染、帧缓冲区。

GLuint defaultRenderbuffer[1], defaultFramebuffer[1];
glGenRenderbuffers(1, defaultRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, defaultRenderbuffer[0]);
[context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];

glGenFramebuffers(1, defaultFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, defaultFramebuffer[0]);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, defaultRenderbuffer[0]);

2、配置多重采样(Multisampled)渲染及帧缓冲区
多重采样渲染缓冲区的内存分配与单采样不同,在此需要手动指定其格式等信息。同时,需要知道单采样渲染缓冲区的大小。

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

有了渲染缓冲区大小信息后,才开始配置多重采样信息。然而,相应的多重采样帧缓冲区与多重采样渲染缓冲区的连接并无变化。

GLuint msaaRenderbuffer[1], msaaFramebuffer[1];
glGenRenderbuffers(1, msaaRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, msaaRenderbuffer[0]);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, 
    4/* 采样倍数 */, 
    GL_RGBA8 /* 渲染缓冲区格式 */, 
    width, height);

glGenFramebuffers(1, msaaFramebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, msaaFramebuffer[0]);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msaaRenderbuffer[0]);

为了确保多重采样相关缓冲区是否成功,在此查询帧缓冲区状态。

// Test the framebuffer for completeness.
if(glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    NSLog(@"failed to make complete framebuffer object %x", 
        glCheckFramebufferStatus(GL_FRAMEBUFFER));
}

3、绑定多重采样渲染及帧缓冲区
在绘制前,需要将当前渲染缓冲区、帧缓冲区指定成前面给多重采样定制的缓冲区。

glBindFramebuffer(GL_FRAMEBUFFER, msaaFramebuffer[0]);
glBindRenderbuffer(GL_RENDERBUFFER, msaaRenderbuffer[0]);

4、设置视口为单采样渲染缓冲区大小
至于视口的设置在前或在后,并不影响绘制结果。

5、绘制
绘制部分内容和正常一样。

6、绑定读缓冲区GL_READ_FRAMEBUFFER
由前面的绘制写在了多重采样帧缓冲区的颜色附着,故令其充当最终成像的数据源。

glBindFramebuffer(GL_READ_FRAMEBUFFER, msaaFramebuffer[0]);

7、绑定绘制缓冲区GL_DRAW_FRAMEBUFFER
绘制缓冲区为最终成像的缓冲区,比如将图像显示到屏幕。

glBindFramebuffer(GL_DRAW_FRAMEBUFFER, defaultFramebuffer[0]);

接着,需要将多重采样帧缓冲区中的颜色、深度和模版等附着拷贝到单采样帧缓冲区。

glBlitFramebuffer(0, 0, width, height,
                  0, 0, width, height,
                  GL_COLOR_BUFFER_BIT,
                  GL_LINEAR);

8、绑定单采样渲染缓冲区
至此,多重采样相关的缓冲区都完成了它们的使命,数据也在上一步操作中拷贝到了单采样渲染缓冲区,此时,应该将它设置为使用状态。

glBindRenderbuffer(GL_RENDERBUFFER, defaultRenderbuffer[0]);

9、交换前后帧缓冲区
和普通采样程序一样,为了最终显示在屏幕上,需要通知iOS、Android等系统交换前后帧缓冲区。

[context presentRenderbuffer:GL_RENDERBUFFER];

2.2、成像效果比较

OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点_第3张图片
确实存在多重采样渲染缓冲区

Renderbuffer #2确认上述代码确实创建了多重采样相关数据。同时,也表明了Renderbuffer #1是单采样缓冲区。接下来,比较GL_RBGA8与GL_RBGA两个internal format宏定义的区别。

1、4倍GL_RBGA8格式的多重采样

OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点_第4张图片
单采样颜色缓冲区

OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点_第5张图片
4倍多重采样颜色缓冲区

理论上,这两个缓冲区的数据是一样的,因此,它们看起来也是一样的,对我而言。

2、屏蔽混合(Blend)的多重采样
注释如下代码,再次运行,比较成像质量。

//  glEnable(GL_BLEND);
//  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点_第6张图片
无Blend、4倍多重采样颜色缓冲区

仔细观察可发现,无Blend情况下,圆点越大则锯齿现象越明显。

3、4倍GL_RBGA格式的多重采样
修改GL_RGBA8成GL_RBGA时,glCheckFramebufferStatus检查帧缓冲区状态不完整,也无法进行后续有效的绘制,所以多重采样的internal format参数与glTexImage2D显示RGBA图片时略有区别。

glRenderbufferStorageMultisample(GL_RENDERBUFFER, 
    4, 
    GL_RGBA,
    width,
    height);

小结

从以上两份代码的表现可推断,单纯绘制点并配合多重采样的实现在Retina屏上并不会像Windows默认DPI下有相对明显的视觉改善。继续探讨多重采样及绘制光滑圆点前,下一篇文档OpenGL ES 3.0 数据可视化 3:多次绘制调用(glDraw*)的性能问题先谈谈当前代码存在的运行性能问题及OpenGL ES工作方式的简要介绍

你可能感兴趣的:(OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点)