测试设备为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);
运行结果为方点,如下所示。
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把点绘制成圆形而非矩形,需要处理光栅化后的点所包含的像素数据,思路是,忽略半径大于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问题,后续文档将会讨论此类问题的优化。
此时,CPU代码也同步修改,每次绘制都进行通知。
glUniform1f(0, GL_TRUE);
glDrawArrays(GL_POINTS, 0, 1);
glUniform1i(0, GL_FALSE);
值得注意的是,定义在vertex shader中的uniform变量并不能跨越到fragment shader中使用,在编译fragment shader时提示为未定义变量,如下所示。
因此,严格地说,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:多重采样绘制光滑圆点。