OpenGL ES 3.0 数据可视化 1:绘制圆点

测试设备为iPad Air 2(iOS 9.2)。10月对本系列文档进行修订且将部分长篇幅的文档拆成更多简短文档,新代码继续使用OpenGL ES 3.0,新测试设备为iPhone7 plus(iOS 10.0.1)。代码托管在GitHub: ES3_1_RoundPoint。

基于上篇文档OpenGL ES 3.0 数据可视化 0:Hello world,本文档开始尝试绘制圆点,对应OpenGL Data Visualization Cookbook书Chapter 2: OpenGL Primitives and 2D Data Visualization的Drawing Points部分内容,使用朴素实现画一系列的点。

欢迎加入GPUImage、OpenGL ES、Vulkan、Metal交流群536987698,一起学习。

1、运行效果

直接绘制的效果:


方点

绘制成圆点的效果:


圆点

2、朴素实现(基于UIView)

为方便Android开发的同学阅读代码,朴素实现代码不使用GLKit,直接继承UIView配置E(A)GL环境。同理,Android也需要配置EGL将OpenGL ES与本地窗口系统进行链接才可继续进行OpenGL操作。iOS开发的同学可直接拷贝代码到Xcode,查看真机上的运行效果。

#import 
#import 

@interface RoundPointView : UIView

@end

@interface RoundPointView () {
    CAEAGLLayer *glLayer;
    EAGLContext *context;
}

@end

@implementation RoundPointView

+ (Class)layerClass {
    return [CAEAGLLayer class];
}

- (void)layoutSubviews {
    [super layoutSubviews];
    
    glLayer = (CAEAGLLayer *)self.layer;
    glLayer.contentsScale = [UIScreen mainScreen].scale;
    
    context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES3];
    [EAGLContext setCurrentContext:context];
    
    GLuint renderbuffer;
    glGenRenderbuffers(1, &renderbuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, renderbuffer);
    [context renderbufferStorage:GL_RENDERBUFFER fromDrawable:glLayer];
    
    GLint renderbufferWidth, renderbufferHeight;
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &renderbufferWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &renderbufferHeight);
    
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, renderbuffer);
    
    char *vertexShaderContent =
        "#version 300 es \n"
        "layout(location = 0) in vec4 position; "
        "layout(location = 1) in float point_size; "
        "void main() { "
            "gl_Position = position; "
            "gl_PointSize = point_size;"
        "}";
    GLuint vertexShader = compileShader(vertexShaderContent, GL_VERTEX_SHADER);
    
    char *fragmentShaderContent =
        "#version 300 es \n"
        "precision highp float; "
        "out vec4 fragColor; "
        "void main() { "
            "fragColor = vec4(1.0, 0.0, 1.0, 1.0);"
        "}";
    GLuint fragmentShader = compileShader(fragmentShaderContent, GL_FRAGMENT_SHADER);
    
    GLuint program = glCreateProgram();
    glAttachShader(program, vertexShader);
    glAttachShader(program, fragmentShader);
    
    glLinkProgram(program);
    GLint linkStatus;
    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
    if (linkStatus == GL_FALSE) {
        GLint infoLength;
        glGetProgramiv(program, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetProgramInfoLog(program, infoLength, NULL, infoLog);
            printf("%s\n", infoLog);
            free(infoLog);
        }
    }
    
    glUseProgram(program);
    
    glEnable(GL_BLEND);
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    
    glClearColor(1, 1, 1, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    
    glViewport(0, 0, renderbufferWidth, renderbufferHeight);

    GLfloat vertex[2];
    GLfloat size[] = {50.f};
    for (GLfloat i = -0.9; i <= 1.0; i += 0.25f, size[0] += 20) {
        vertex[0] = i;
        vertex[1] = 0.f;
        
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 2/* 坐标分量个数 */, GL_FLOAT, GL_FALSE, 0, vertex);
        
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, size);

        glDrawArrays(GL_POINTS, 0, 1);
    }
    
    [context presentRenderbuffer:GL_RENDERBUFFER];
}

GLuint compileShader(char *shaderContent, GLenum shaderType) {
    GLuint shader = glCreateShader(shaderType);
    glShaderSource(shader, 1, &shaderContent, NULL);
    glCompileShader(shader);
    
    GLint compileStatus;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
    if (compileStatus == GL_FALSE) {
        GLint infoLength;
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &infoLength);
        if (infoLength > 0) {
            GLchar *infoLog = malloc(sizeof(GLchar) * infoLength);
            glGetShaderInfoLog(shader, infoLength, NULL, infoLog);
            printf("%s -> %s\n", shaderType == GL_VERTEX_SHADER ? "vertex shader" : "fragment shader", infoLog);
            free(infoLog);
        }
    }
    return shader;
}

@end

3、实现代码与分析

3.1、设置点大小

原书通过glPointSize(size)设置所绘制点的大小,OpenGL ES 1.0有此函数。然而,OpenGL 2.0及3.0已删除,设置点大小与绘制点功能是两部分操作,在此先介绍设置点大小。

/////////////上传点大小至GPU
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, size);

3.2、在顶点着色器中指定点的大小

由前面可知,OpenGL 2.0及以上版本已移除绘点函数,那么此功能目前可在顶点着色器中实现,如下所示。

gl_PointSize = point_size;

gl_PointSize是光栅化后的点大小,单位为像素。gl_PointSize有大小限制,超过限制后,OpenGL ES不进行绘制操作。若想绘制超过限制大小的点,可考虑使用绘制圆(比如,GL_TRIANGLE_FAN)实现。

gl_PointSize is used to determine the size of rasterized points, otherwise it is ignored by the rasterization stage.

3.3、绘制点

原书绘制代码由glBegin、glEnd等OpenGL立即模式接口实现,代码如下所示。

void drawPoint(Vertex v1, GLfloat size) {
    glPointSize(size);
    glBegin(GL_POINTS);
    glColor4f(v1.r, v1.g, v1.b, v1.a);
    glVertex3f(v1.x, v1.y, v1.z);
    glEnd();
}

同样,需要替换为OpenGL ES 3.0的操作。

vertex[0] = i;
vertex[1] = 0.f;
        
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2/* 坐标分量个数 */, GL_FLOAT, GL_FALSE, 0, vertex);
        
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, size);

glDrawArrays(GL_POINTS, 0, 1);

运行结果为方点,如下所示。

OpenGL ES 3.0 数据可视化 1:绘制圆点_第1张图片
单个方点

3.4、修改片段着色器实现圆点绘制

原书通过启用GL_POINT_SMOOTH及混合函数实现了圆点及抗锯齿功能,代码如下所示。

glEnable(GL_POINT_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
OpenGL ES 3.0 数据可视化 1:绘制圆点_第2张图片
原书抗锯齿圆点

为了让OpenGL ES 3.0把点绘制成圆形而非矩形,需要处理光栅化后的点所包含的像素数据,思路是,忽略半径大于0.5的点,从而实现圆点绘制。类似gl_Texcoord,Fragment Shader内置变量gl_PointCoord提供了当前片段在所绘制的点中的位置,值范围为[0, 1]。下面代码将超过半径的片段舍弃,实现了将方形裁剪成圆形的目标。

if (length(gl_PointCoord - vec2(0.5)) > 0.5)
    discard;
}

另外,为避免影响其他内容的绘制,在点绘制结束时让OpenGL ES恢复常规处理,故加入了一个布尔类型的统一变量is_sprite,修改后的片段着色器代码如下所示。

uniform bool is_sprite;
// void main()
if (is_sprite) {
    if (length(gl_PointCoord-vec2(0.5)) > 0.5)
        discard;
    }
else
    fragColor = v_point_color;

使用discard关键字在有深度缓冲区的场合中容易导致Loss Of Depth Test Hardware Optimizations问题,后续文档将会讨论此类问题的优化。

Loss Of Depth Test Hardware Optimizations

此时,CPU代码也同步修改,每次绘制都进行通知。

glUniform1f(0, GL_TRUE);
glDrawArrays(GL_POINTS, 0, 1);
glUniform1i(0, GL_FALSE);

值得注意的是,定义在vertex shader中的uniform变量并不能跨越到fragment shader中使用,在编译fragment shader时提示为未定义变量,如下所示。

OpenGL ES 3.0 数据可视化 1:绘制圆点_第3张图片
uniform并不能在vertex、fragment中共享

因此,严格地说,uniform类型变量只是在当前program中所有的顶点着色器或者在所有片段着色器中共享,而不是在所有顶点着色器及片段着色器中共享。

两个思考题:

Did you consider using a small texture (containing a circle) instead of doing a calculation like this? It might be a bit faster but it obviously depends on the details.
Also try to avoid using the discard keyword. It might have a negative impact on performance. You could for example set the alpha value to 0 for those fragments that you currently discard.

参考与推荐阅读

  • Computer Graphics Through OpenGL
  • WebGL Programming Guide: Interactive 3D Graphics Programming with WebGL

下一篇文档OpenGL ES 3.0 数据可视化 2:多重采样绘制光滑圆点。

你可能感兴趣的:(OpenGL ES 3.0 数据可视化 1:绘制圆点)