OpenGL 概念整理

1. OpenGL

1.1 术语说明

概念描述版本
OpenGLOpen Graphics Library Khronos 定义GPU功能实现的SPEC,标准API4.6
OpenGL ESEmbedded System 为嵌入式系统定义的相关补充接口3.2
EGL用于GPU渲染与机器原生窗口之间通信的API,独立于OpenGL ES各个版本
VulkanKhronos组织新定义的接口,相比于OpenGL更加高效1.2

1.2 OpenGL 理解

从功能上理解,OpenGL 是开发者操作GPU渲染的工具,需要将所定义的行为给到GPU去执行渲染操作:

  1. 将绘制源数据提供给到GPU;
  2. 将执行操作(程序)给到GPU;
  3. 绘制完成后将显示内容提供给到framebuffer;

针对于上述功能需求,Khronos将OpenGL设计成为一组状态机,规定一系列的变量描述OpenGL此刻应当如何运行,其状态通常被称为上下文(Context),我们可以在程序中配置他们,这些状态会一直生效到下次改变

  • Context:作为状态机的上下文,描述当前状态的值以及改变状态的值;
  • Object: OpenGL将一系列具有相同功能的状态抽象出状态的子集称之为对象
  • OpenGL Shading Language:GPU的编程语言,使用GLSL来编写shader程序
概念说明
Vertex顶点 GPU渲染绘制是对于点的操作,这里是物体顶点坐标的计算操作
Fragment片元操作,理解为对于像素点的颜色计算操作
Shader着色器:GPU中执行程序的名称(ARM 将HW 叫做Shader Core),一般有Vertex、fragment、geometry等
Texture作为Fragment输入操作的源数据,多种方式提供给到GPU

1.3 VBO、EBO、VAO的理解

顶点数据如何提供给到GPU:

对象说明
VBOVertex Buffer Object,一块buffer,描述顶点的数据对象
EBOElement Buffer Object,描述元素的数据对象,存储顶点的索引,可以复用顶点构建三角形
VAOVertex Arrary Object,一块buffer,顶点数组对象,可以描述多个顶点

image-20210218174723184

GLfloat vertices[] = {
    0.5f, 0.5f, 0.0f,   // 右上角
    0.5f, -0.5f, 0.0f,  // 右下角
    -0.5f, -0.5f, 0.0f, // 左下角
    -0.5f, 0.5f, 0.0f   // 左上角
};

GLuint indices[] = { // 注意索引从0开始! 
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
};

	GLuint VBO, VAO, EBO;//声明变量

    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO); //生成对应buffer

 	glBindVertexArray(VAO); //绑定VAO

 	glBindBuffer(GL_ARRAY_BUFFER, VBO);//绑定VBO 将我们定义的顶点数据填充到VBO中
 	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);绑定EBO 并填充数据
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);//设置顶点属性指针,这里是描述VAO中各个数据的含义

    glEnableVertexAttribArray(0); //解除绑定
    glBindBuffer(GL_ARRAY_BUFFER, 0); 
    glBindVertexArray(0); 

上述code建立了VAO的数据索引,是初始化过程,后续在实际绘制时绑定VAO即可使用上述数据;

1.4 Shader

  • Shader是运行在GPU上的程序,这些程序是Pipeline中某一个特定的环节执行(所谓vertex、fragment、geometry shader的可编程特性就是这里实现的)

  • Shader只是一种把输入转化为输出的程序。着色器也是一种非常独立的程序,因为它们之间不能相互通信;它们之间唯一的沟通只有通过输入和输出。

  • Shader 程序使用类C语言实现,即上文提到的GLSL;

典型Shader程序基本机构如下:

#version version_number //声明版本号,特性不同

in type in_variable_name;//声明输入变量
in type in_variable_name;

out type out_variable_name;//声明输出变量

uniform type uniform_name;// 声明uniform变量

int main()//主程序
{
  // 处理输入并进行一些图形操作
  ...
  // 输出处理过的结果到输出变量
  out_variable_name = weird_stuff_we_processed;
}

Uniform是一种从CPU中的应用向GPU中的着色器发送数据的方式,但uniform和顶点属性有些不同。首先,uniform是全局的(Global)。全局意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。第二,无论你把uniform值设置成什么,uniform会一直保存它们的数据,直到它们被重置或更新。

1.5 标准化设备坐标

OpenGL 中的坐标如下图所示:

image-20210218174109608

X轴和Y轴都是从(-1, 1)范围,对应到实际屏幕上

2. OpenGL ES 介绍

OpenGL ES 是 OpenGL API的子集,针对嵌入式设备设计;

OpenGL ES 是从 OpenGL 裁剪的定制而来的,去除了glBegin/glEnd,四边形(GL_QUADS)、多边形(GL_POLYGONS)等复杂图元等许多非绝对必要的特性;

3. EGL 介绍

EGL 是 渲染API 和原生窗口系统之间的接口;

  • 可以理解为GPU渲染完成后需要将out数据丢到framebuffer或者其他buffer中用于后续显示操作,所以这部分与平台特性强相关,所以抽象出来一组标准接口,作为GPU渲染与Display显示的桥梁;

提供如下机制:

  • 与设备原生窗口通信
  • 查询绘制surface的可用类型和配置
  • 创建绘制surface
  • 同步渲染
  • 管理纹理贴图等渲染资源

主要接口如下:

  1. 检查错误:EGL API 成功返回EGL_TRUE,失败返回EGL_FALSE,具体原因需要调用如下接口

    EGLint eglGetError();
    
  2. 创建本地系统和OpenGL ES的连接并进行初始化

    //创建display
    EGLDisplay eglDisplay(EGLNativeDisplayType displayId);
    //初始化
    EGLBoolean eglInitialize(EGLDisplay display, // 创建步骤时返回的对象
                             EGLint *majorVersion, // 返回 EGL 主版本号
                             EGLint *minorVersion); // 返回 EGL 次版本号
    
  3. 找到可用surface配置项

    //获取所有配置
    EGLBoolean eglGetConfigs(EGLDisplay display, // 指定显示的连接
                             EGLConfig *configs, // 指定 GLConfig 列表
                             EGLint maxReturnConfigs, // 最多返回的 GLConfig 数
                             EGLint *numConfigs); // 实际返回的 GLConfig 数
    //查询EGL Config配置
    EGLBoolean eglGetConfigAttrib(EGLDisplay display, // 指定显示的连接
                                  EGLConfig config, // 指定要查询的 GLConfig
                                  EGLint attribute, // 返回特定属性
                                  EGLint *value); // 返回值
    // 确认配置
    EGLBoolean eglChooseChofig(EGLDispay display, // 指定显示的连接
                               const EGLint *attribList, // 指定 configs 匹配的属性列表,可以为 NULL
                               EGLConfig *config,   // 调用成功,返会符合条件的 EGLConfig 列表
                               EGLint maxReturnConfigs, // 最多返回的符合条件的 GLConfig 数
                               ELGint *numConfigs );  // 实际返回的符合条件的 EGLConfig 数
    
  4. 创建Surface

    EGLSurface eglCreateWindowSurface(EGLDisplay display, // 指定显示的连接
                                      EGLConfig config, // 符合条件的 EGLConfig
                                      EGLNatvieWindowType window, // 指定原生窗口
                                      const EGLint *attribList); // 指定窗口属性列表,可为 NULL
    
  5. 创建并关联上下文

    //创建上下文
    EGLContext eglCreateContext(EGLDisplay display, // 指定显示的连接
                                EGLConfig config, // 前面选好的 EGLConfig
                                EGLContext shareContext, // 允许其它 EGLContext 共享数据,使用 EGL_NO_CONTEXT 表示不共享
                                const EGLint* attribList); // 指定操作的属性列表,只能接受一个属性 EGL_CONTEXT_CLIENT_VERSION 
    //关联上下文
    EGLBoolean eglMakeCurrent(EGLDisplay display, // 指定显示的连接
                              EGLSurface draw, // EGL 绘图表面
                              EGLSurface read, // EGL 读取表面
                              EGLContext context); // 指定连接到该表面的上下文
    

如下为完整流程举例说明:

ret = modeset_open(&fd, "/dev/dri/card0");
//...
ret = modeset_prepare(fd);
//...
gbm = gbm_create_device(fd);
//...	
gbm_surface = gbm_surface_create(gbm, pws_current->ws_width, pws_current->ws_height, GBM_FORMAT_ARGB8888, 0);
//...
egl_display = eglGetDisplay(gbm);
//...
ret = eglInitialize(egl_display, NULL, NULL);
//...
ret = eglGetConfigs(egl_display, NULL, 0, &max_config_num);
//...
pconfigs = (EGLConfig*)malloc(sizeof(EGLConfig) * max_config_num);
//...
ret = eglChooseConfig(egl_display, attr, pconfigs, max_config_num, &max_config_num);
//...
int i=0;
for ( i=0; i<max_config_num; i++ )
{
    EGLint value;
    /*Use this to explicitly check that the EGL config has the expected color depths */
    eglGetConfigAttrib( egl_display, pconfigs[i], EGL_RED_SIZE, &value );
    if ( 8 != value ) continue;
    eglGetConfigAttrib( egl_display, pconfigs[i], EGL_GREEN_SIZE, &value );
    if ( 8 != value ) continue;
    eglGetConfigAttrib( egl_display, pconfigs[i], EGL_BLUE_SIZE, &value );
    if ( 8 != value ) continue;
    eglGetConfigAttrib( egl_display, pconfigs[i], EGL_ALPHA_SIZE, &value );
    if ( 8 != value ) continue;
    eglGetConfigAttrib( egl_display, pconfigs[i], EGL_SAMPLES, &value );
    if ( 4 != value ) continue;
    printf("-------use config[%d] ------\r\n", i);
    ecfg = pconfigs[i];
    break;
}

egl_surface = eglCreateWindowSurface(egl_display, ecfg, (NativeWindowType)gbm_surface, NULL);
//...
egl_context = eglCreateContext(egl_display, ecfg, EGL_NO_CONTEXT, ctxattr);
//...
eglMakeCurrent(egl_display, egl_surface, egl_surface, egl_context);

上述为标准的EGL获取display的path,其目的是获取到显示device和申请到surface用于后续buffer调换;

4. buffer流转图示

单buffer:image-20210219120000678

如上图,只申请一个buffer进行流转的情况,由于只有一个buffer,所以存在Display模块正在显示的时候GPU同时绘制,则出现撕裂;

双buffer:

双buffer缓冲机制即添加同步操作,避免不同的user操作同一个buffer的行为:

  • GPU在swapbuffer操作时对当前idle状态buffer进行渲染操作;
  • 渲染完成后调用page flip操作将render完成的buffer地址给到Display模块;
  • Display模块会在下次vsync信号过来,显示该buffer数据;

双buffer可以有效的避免buffer绘制时显示异常的问题(比如撕裂),但是由于render和display操作耗时不同,会存在等待耗时较长的操作的情况(一般是等待render);

多buffer流转:image-20210219124146760

多buffer流转可以解决上述等待的情况,可以理解为将render和show操作分离,或者将render时间与show时间的差值划分到各个buffer中;

注意一定做好同步操作,否则会出现撕裂抖帧等问题

  • GPU 从Idle queue中获取到buffer进行渲染操作,完成后将其添加到busy queue中;
  • Display模块从Busy queue中获取到buffer进行显示操作,在下次Vsync过来时替换buffer显示,如果此时Busy queue中为空,则仍显示此帧数据,并且不释放这个buffer;
  • 如果GPU申请Idle buffer时为空,则等待show完成,一般来说合理的buffer数量为3~5个,也可以动态申请,即在没有buffer时动态申请添加;
Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐