OpenGL ES iOS笔记01 使用OpenGL绘制三角形

因为工作需要,需要了解下OpenGL的基本使用和原理,以及ios下的滤镜框架GPUImage的实现原理,因此在此记录一下。

OpenGL渲染简单流程

OpenGL渲染简单流程.png

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。

为了描述3D空间中的点,就需要一个三维坐标,一个三维坐标的数组叫做顶点数据(Vertex Data),这些顶点数据描述了需要绘制的图形的点,可以指定这些点渲染成点、线或者三角形。中间经过图元装配阶段、几何着色器处理阶段、光栅化阶段,然后进入片段着色器,来计算一个像素的最终颜色,之后还会经过Alpha测试和混合(Blending)阶段,然后得出最终的像素颜色。

图元装配(Primitive Assembly)阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状.

图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。

几何着色器的输出会被传入光栅化阶段(Rasterization Stage),这里它会把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

大多数情况下,只需要关系顶点着色器和片段着色器的处理即可。在现代OpenGL中,必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。

在iOS环境下使用OpenGL流程

创建自定义view并修改layer

在iOS中一般使用CAEAGLLayer来实现OpenGL的各种功能,一般使用自定义的UIView,并改变他的layer的类型。CAEAGLLayer默认是透明的,官方建议设为不透明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@interface LYRGLView ()
@property(nonatomic,strong)CAEAGLLayer*eaglLayer;
@end
@implementation LYRGLView
+(Class)layerClass
{
    return [CAEAGLLayer class];
}
-(void)prepareLayer
{
    self.eaglLayer = (CAEAGLLayer*)self.layer;
    //设置不透明,节省性能
    self.eaglLayer.opaque = YES;
}
@end

创建Context上下文

1
2
3
4
5
6
7
8
9
@property(nonatomic,strong)EAGLContext*context;

...

-(void)prepareContext
{
    self.context = [[EAGLContext alloc]initWithAPI:kEAGLRenderingAPIOpenGLES2];
    [EAGLContext setCurrentContext:self.context];
}

创建render buffer(渲染缓存)

render buffer用来存储即将绘制到屏幕上的图像数据,理解为帧缓冲的一个附件,用来真正存储图像的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface LYRGLView ()
{
    GLuint _renderBuffer;
}
....

-(void)prepareRenderBuffer{
    glGenRenderbuffers(1, &_renderBuffer);
    glBindRenderbuffer(GL_RENDERBUFFER, _renderBuffer);
    //调用这个方法来创建一块空间用于存储缓冲数据,替代了glRenderbufferStorage
    [self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.eaglLayer];
}

创建frame buffer(帧缓冲)

帧缓冲理解为多种缓冲的结合。

1
2
3
4
5
6
7
8
9
10
-(void)prepareFrameBuffer
{
    GLuint framebuffer;
    glGenFramebuffers(1, &framebuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, framebuffer);
    //附加之前的_renderBuffer
    //GL_COLOR_ATTACHMENT0指定第一个颜色缓冲区附着点
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
                              GL_RENDERBUFFER, _renderBuffer);
}

创建并编译shader

shader使用着色器语言GLSL(OpenGL Shading Language)编写,主要需要编写顶点着色器和片段着色器的实现,顶点着色器将计算好的顶点传入片段着色器,然后片段着色器计算像素最后的颜色输出。

GLSL语言基础

这里定义两个shader,其中顶点shader直接将顶点坐标传给片段shader,片段shader将像素颜色固定为绿色。shader的具体代码使用NSString字符串常量保存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//方便定义shader字符串的宏
#define STRINGIZE(x) #x
#define STRINGIZE2(x) STRINGIZE(x)
#define SHADER_STRING(text) @ STRINGIZE2(text)

//顶点着色器
NSString *const vertexShaderString = SHADER_STRING
(
 //attribute 关键字用来描述传入shader的变量
 attribute vec4 vertexPosition; //传入的顶点坐标
 void main(void) {
     gl_Position = vertexPosition; // gl_Position是vertex shader的内建变量,gl_Position中的顶点值最终输出到渲染管线中
 }
);
//片段着色器
NSString *const fragmentShaderString = SHADER_STRING
(
 void main(void) {
 	//设置为绿色
    gl_FragColor = vec4(0, 1, 0, 1); // gl_FragColor是fragment shader的内建变量,gl_FragColor中的像素值最终输出到渲染管线中
 }
);

接下来需要使用这些字符串常量创建并编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-(void)prepareShader
{
    //创建顶点着色器
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);

    const GLchar* const vertexShaderSource =  (GLchar*)[vertexShaderString UTF8String];
    GLint vertexShaderLength = (GLint)[vertexShaderString length];
    //读取shader字符串
    glShaderSource(vertexShader, 1, &vertexShaderSource, &vertexShaderLength);
    //编译shader
    glCompileShader(vertexShader);

    //创建片元着色器
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    const GLchar* const fragmentShaderSource = (GLchar*)[fragmentShaderString UTF8String];
    GLint fragmentShaderLength = (GLint)[fragmentShaderString length];
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, &fragmentShaderLength);
    glCompileShader(fragmentShader);
    
}

创建着色器程序并链接shader

着色器程序对象是多个着色器合并之后并最终链接完成的版本。需要将刚才创建的shader链接至着色器程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@interface LYRGLView ()
{
    GLuint _renderBuffer;
    //着色器程序
    GLuint _glprogram;
}
...

-(void)prepareShader
{
...

//创建glprogram
    _glprogram = glCreateProgram();
    
    //绑定shader
    glAttachShader(_glprogram, vertexShader);
    glAttachShader(_glprogram, fragmentShader);
    //链接program
    glLinkProgram(_glprogram);
    
    //选择程序对象为当前使用的程序,类似setCurrentContext
    glUseProgram(_glprogram);

传入顶点数据,完成三角形绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-(void)render {
	//shader中vertexPosition参数的索引,因为是只有一个参数,所以是0,也可以使用glGetAttribLocation函数,传入_glprogrem和参数名称字符串查找
	int vertexPositionIndex = 0;
	//启用attribute变量,使其对GPU可见,默认为关闭
   	glEnableVertexAttribArray(vertexPositionIndex);
   	
   	//绘制三角形需要三个坐标,由于是屏幕,所以z的值都为0。OpenGL的坐标系是以中心为原点的,所以(1,1)在右上角
   	const float vertices[] = {
        1.0f, 1.0f, 0.0f,   // 右上角
        1.0f, -1.0f, 0.0f,  // 右下角
        -1.0f, -1.0f, 0.0f, // 左下角
    };
    
    //顶点坐标对象
    GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    //将顶点坐标写入顶点VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    //告诉OpenGL该如何解析顶点数据
    //每个顶点属性从一个VBO管理的内存中获得它的数据,而具体是从哪个VBO(程序中可以有多个VBO)获取则是通过在调用glVetexAttribPointer时绑定到GL_ARRAY_BUFFER的VBO决定的。由于在调用glVetexAttribPointer之前绑定的是先前定义的VBO对象,顶点属性0现在会链接到它的顶点数据。
    glVertexAttribPointer(vertexPositionIndex, 3, GL_FLOAT, GL_FALSE, sizeof(float)*3, (void*)0);
    
    //清屏为白色
    glClearColor(1.0, 1.0, 1.0, 1.0);
    glClear(GL_COLOR_BUFFER_BIT);
    //设置gl渲染窗口大小
    glViewport(0, 0, self.frame.size.width, self.frame.size.height);
    //绘制三个顶点的三角形
	glDrawArrays(GL_TRIANGLES, 0, 3);
    
    //EACAGLContext 渲染OpenGL绘制好的图像到EACAGLLayer
    [_context presentRenderbuffer:GL_RENDERBUFFER];
}

最后创建一个自定义view,并在init方法中调用上述方法,即可渲染出三角形。
<img src=”https://i.loli.net/2019/06/15/5d047c69a2de390910.png” alt=”OpenGL01三角.png” title=”OpenGL01三角.png” width=36% />

使用索引缓冲对象

画一个矩形

画一个占据屏幕的矩形,需要两个三角形,也就是6个顶点,可以将之前的顶点坐标数据修改,再将绘制是的顶点数量由3修改为6即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
float vertices[] = {
    // 第一个三角形
    1.0f, 1.0f, 0.0f,   // 右上角
    1.0f, -1.0f, 0.0f,  // 右下角
    -1.0f, 1.0f, 0.0f,  // 左上角
    // 第二个三角形
    1.0f, -1.0f, 0.0f,  // 右下角
    -1.0f, -1.0f, 0.0f, // 左下角
    -1.0f, 1.0f, 0.0f   // 左上角
};
...

glDrawArrays(GL_TRIANGLES, 0, 6);

<img src=”https://i.loli.net/2019/06/15/5d047c686d22211739.png” alt=”OpenGL01矩形.png” title=”OpenGL01矩形.png” width= 36%/>

这样就会发现,6个顶点中有两个顶点是重复的,如果绘制成千上万个图形,带来的内存开支是非常多的,这时候就可以使用索引缓冲对象,顶点数组存放不重复的顶点,然后将顶点的索引以绘制的形状来存储,可以避免上面的问题。

使用索引绘制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-(void)render {
    ...
    float vertices[] = {
        1.0f, 1.0f, 0.0f,   // 右上角
        1.0f, -1.0f, 0.0f,  // 右下角
        -1.0f, 1.0f, 0.0f,  // 左上角
        -1.0f, -1.0f, 0.0f, // 左下角
    };
    
    const GLint Indices[] = {
        0, 1, 2,
        2, 3, 1
    };
    
    //顶点坐标对象
    GLuint vertexBuffer;
    glGenBuffers(1, &vertexBuffer);
    glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer);
    //将顶点坐标写入顶点VBO
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
    
    
    //索引
    GLuint indexBuffer;
    glGenBuffers(1, &indexBuffer);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, indexBuffer);
    //将顶点索引数据写入索引缓冲对象
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(Indices), Indices, GL_STATIC_DRAW);
    
    ...
    
	//    glDrawArrays(GL_TRIANGLES, 0, 6);
    glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);    
}

运行之后,也可以显示出绿色的矩形。

demo地址

参考:
你好,三角形
GLSL语言基础
OpenGL ES Programming Guide