文章目录

  • 1. 介绍
  • 2. 顶点输入
  • 3. 顶点着色器
  • 4. 编译着色器
  • 5. 片元着色器
  • 6. 着色器程序
  • 7. 链接顶点属性
  • 8. 顶点数组对象
  • 9. 索引缓冲对象

1. 介绍

在OpenGL中,所有要素都是三维的,但屏幕却是二维的,因此在渲染过程中,需要将3D坐标转换为适应屏幕的2D坐标,其处理过程由图形渲染管线Graphics Pipeline)管理。包括将3D坐标转换为2D坐标;将2D坐标转换为实际的有颜色的像素两个部分。(2D坐标是指点在二维空间的位置,而2D像素是这个点的近似值,受屏幕分辨率的限制)。

其中图形渲染管线可以被划分为几个阶段,每个阶段会把前一阶段的输出作为输入。在每个阶段快速处理数据的小程序称之为着色器(shader)。着色器在GPU中运行,使用OpenGL着色器语言(OpenGL Shading Language,GLSL)编写。

下图是一个图形渲染管线每个阶段的抽象展示,蓝色部分可以自定义着色器内容。

首先以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形。这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。一个顶点(Vertex)是一个3D坐标,而顶点数据用顶点属性(Vertex Attribute)表示,可以包含位置和颜色等信息。在OpenGL渲染过程中,需要指定数据表示的渲染类型,即是点,还是三角形等,即图元(Primitive),任何一个绘制指令的调用都需要将图元传递给OpenGL。

(1)顶点着色器(Vertex Shader)作为第一个阶段,将单独的顶点作为输入。顶点着色器运行对顶点属性进行一些基本处理。
(2)图元装配(Primitive Assembly)将顶点着色器输出的所有顶点作为输入,并将所有的点装配为指定图元的形状。
(3)几何着色器(Geometry Shader)将图元的顶点集合作为输入,可以通过产生新顶点构造出新的图元来生成其他形状。
(4)光栅格化(Rasterization Stage)将图元映射为最终屏幕上相应的像素,生成供片元着色器使用的片元(Fragment)。在片元着色器运行之前会进行裁剪(Clipping),将超出视图范围外的像素丢弃,以提升效率。
(5)片元着色器(Fragment Shader)用于计算每个像素的最终颜色。
(6)测试和混合(Test and blending)阶段,用来检测片元的深度,判断物体的前后位置关系。

在OpenGL中,必须至少定义一个顶点着色器和一个片元着色器(GPU中没有默认的),几何着色器可选,通常直接使用默认的。

2. 顶点输入

在开始绘制图形之前,需要先给OpenGL输入一些顶点数据,这些顶点都是3D坐标格式(x,y,z),坐标值在-1.0至1.0范围内才会被处理。即在标准化设备坐标(Normalized Device Coordinate)范围内的坐标才会最终呈现在屏幕上(在范围外的都不会显示)。

如一个三角形的顶点数组(三个顶点的z坐标均为0,即深度一致):

float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f,  0.5f, 0.0f
};

标准化设备坐标:
当顶点坐标在顶点着色器中处理之后,就会形成标准设备坐标(三个坐标值都在-1.0到1.0之间)。在之后的处理过程中,标准化设备坐标会变换为屏幕空间坐标(Screen-sapce coordinates),即glViewport函数通过视口变换(Viewport Transform)完成的。得到的屏幕坐标将会被输入到片元着色器中。

顶点数据在输入顶点着色器之前,需要在GPU上存储,通过顶点缓冲对象(Vertex Buffer Objects, VBO)管理这个内存,它会在GPU内存(显存)中存储大量的顶点,可以一次性将大批量的顶点数据发送到显卡,减少时间消耗,其中CPU和显卡之间的通信较慢,顶点着色器访问显存中的顶点数据非常快。

顶点缓冲对象有唯一的ID,可以使用glGenBuffers函数和一个ID生成一个VBO对象。

unsigned int VBO;
glGenBuffers(1, &VBO);

OpenGL有多种缓冲对象类型,顶点缓冲对象的类型是GL_ARRAY_BUFFER。OpenGL允许同时绑定多个缓冲(不同类型),可以使用glBindBuffer函数将新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);

GL_ARRAY_BUFFER上的任何缓冲调用都会用来配置当前绑定的缓冲(VBO),然后调用glBufferData函数,将之前定义的顶点数据复制到缓冲的内存中。

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData函数专门用来将顶点数据绑定到缓冲中。第一个采纳数是目标缓冲的类型;第二个参数是输出数据的大小(以字节为单位);第三个参数是待发送的实际数据;第四个参数是指定显卡如何管理给定的数据,有三种形式:

  • GL_STATIC_DRAW:数据不会或者几乎不会改变
  • GL_DYNAMIC_DRAW:数据会被改变很多
  • GL_STAREAM_DRAW:数据每次绘制时都会改变

3. 顶点着色器

顶点着色器(Vertex Shader)作为可编程的着色器之一,使用着色器语言GLSL(OpenGL Shading Language)编写,如下:

#version 330 core
layout (location = 0) in vec3 aPos;void main()
{gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

#version 330 core表示OpenGL的版本号及使用核心模式。(对输入数据,只传输未处理)。

in关键字,在顶点着色器中声明所有的输入顶点属性(Input Vertex Attribute)。顶点都为3D坐标,因此创建vec3类型的输入变量aPos

顶点着色器需要将数据赋值给预定义的gl_Position变量(顶点着色器的输出,vec4类型,w分量设置为1.0f)。

4. 编译着色器

首先将顶点着色器编码在C风格的字符串中:

const char *vertexShaderSource = "#version 330 core\n""layout (location = 0) in vec3 aPos;\n""void main()\n""{\n""   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n""}\0";

为了让OpenGL使用,必须在运行时动态编译。

首先需要创建一个着色器对象,使用ID进行引用。将顶点着色器存储为unsigned int类型,然后使用glCreateShader创建,参数设置为顶点着色器(GL_VERTEX_SHADER

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

然后将着色器源码附加到着色器对象上,进行编译。

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource函数第一个参数为着色器对象;第二个参数为传递的源码字符串数量;第三个参数是着色器的源码。

在调用glCompileShader编译着色器后,检查是否编译成功,并输出错误信息。定义一个整型变量用来表示是否编译成功,使用glGetShaderiv检查是否编译成功,若编译失败,则使用glGetShaderInfoLog获取错误信息。

int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);if(!success)
{glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

5. 片元着色器

片元着色器(Fragment Shader)用于计算像素最后的颜色输出,OpenGL中颜色使用vec4类型表示,即RGBA,每个分量的值在0.0至1.0之间。

#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}

该片元着色器只有一个输出变量,便是最终的输出颜色。声明输出变量使用out关键字。编译片元着色器的过程与顶点着色器类似,只是需要使用GL_FRAGMENT_SHADER常量作为着色器类型:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

当两个着色器都编译完成后,需要将两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。

6. 着色器程序

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接的版本。若需要使用刚才编译的着色器,则需要将其链接(Link)为一个着色器程序对象,然后在渲染对象时激活这个着色器程序。

当链接着色器至一个程序时,它会将每个着色器的输出链接到下个着色器的输入,当输出和输入不匹配时,将会发生连接错误。

创建程序对象:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glCreateProgram函数创建一个程序,并返回新创建程序对象的ID引用。并将之前编译的着色器附加到程序对象上,然后使用glLinkProgram链接。

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

使用glGetProGramivglGetProgramInfoLog检查链接着色器程序是否成功:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);...
}

使用glUseProgram函数激活程序对象:

glUseProgram(shaderProgram);

激活后每个每个着色器的调用和渲染调用都会使用这个程序对象,即将着色器链接到程序对象后,就可以删除原来的着色器对象了。

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

7. 链接顶点属性

在渲染前,需要指定输入数据中的哪一个部分对应顶点着色器的哪一个顶点属性。

顶点缓冲数据会被解析为以下示例:

即:

  • 位置数据被存储为32位(4字节)浮点值
  • 每个位置包含x,y,z共3个值,且在数组中紧密排列(Tightly Packed)
  • 数据中第一个值在缓冲开始的位置

然后可以使用glVertexAttribPointer函数,用于在OpenGL中解析顶点数据。

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer函数参数介绍:

  • 第一个参数:指定要配置的顶点属性,在上文使用了layout(location = 0)定义了Position顶点属性的位置值,因此参数值设置为0。
  • 第二个参数指定顶点属性的大小。顶点属性类型是vec3,因此大小是3。
  • 第三个参数指定数据的类型,这里是GL_FLOAT(GLSL中vec*都是浮点类型)
  • 第四个参数用于指定是否希望数据被标准化(Normalize),若设置为GL_TRUE,则所有数据会被映射到0-1之间(对于有符号的signed数据是-1至1之间),这里设置为GL_FALSE。
  • 第五个参数是步长(Stride),即连续的顶点属性组之间的间隔。下组顶点数组在3个float之后,因此步长设置为3*sizeof(float)。也可以设置为0,让OpenGL决定具体步长(只有当数值紧密排列时才能使用)。
  • 最后一个参数的类型是void*,它表示未知数据在缓冲中起始位置的偏移量。由于位置在数组的开头,因此设置为0。

每个顶点属性从一个VBO管理的内存中获得其数据,在调用glVertexAttribPointer之前绑定预先定义的VBO对象,顶点属性0会链接到它的顶点数据。

定义了OpenGL如何解释顶点数据后,然后使用glEnableVertexAttribArray启用顶点属性(默认禁用),即在OpenGL中绘制一个物体,代码示例如下:

// 0. 复制顶点数组到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();

所有物体绘制都必须重复上述过程,必然会造成繁琐的步骤,因此可以使用顶点数据对象进行优化。

8. 顶点数组对象

顶点数组对象(Vertex Array Object,VAO)会将顶点属性的调用存储。当配置顶点属性指针时,只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就可以了。这使得不同顶点数据和属性配置之间的切换非常简单,值需要绑定不同的VAO,VAO中存储了上文设置的所有状态。
一个VAO会存储:

  • glEnableVertexAttribArrayglDisplayVertexAttribArray的调用
  • 通过glVertexAttribPointer设置的顶点属性配置
  • 通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象


创建VAO与VBO类似:

unsigned int VAO;
glGenVertexArrays(1, &VAO);

使用glBindVertexArray绑定VAO之后才能使用。

// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);[...]// ..:: 绘制代码(渲染循环中) :: ..
// 4. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();

当绘制多个物体时,首先需要生成和配置所有VAO,在绘制物体过程中绑定相应的VAO,绘制完成后再解绑。

9. 索引缓冲对象

当需要绘制一个矩形时,可以通过绘制两个三角形来组成一个矩形(OpenGL主要处理三角形),则顶点集合如下:

float 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,  // 右下角-0.5f, -0.5f, 0.0f, // 左下角-0.5f, 0.5f, 0.0f   // 左上角
};

虽然矩阵只需要4个顶点,但上文顶点集合存储了6个顶点,造成了资源的浪费。更好的解决方案是只存储不同的顶点,并设定这些顶点的绘制顺序,即索引缓冲对象(Element Buffer Object,EBO)

EBO专门存储索引,OpenGL调用这些顶点的索引来决定该绘制哪个点,即索引绘制(Index Drawing)。即首先定义不重复的顶点和绘制索引:

float 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   // 左上角
};unsigned int indices[] = { // 注意索引从0开始! 0, 1, 3, // 第一个三角形1, 2, 3  // 第二个三角形
};

上文只定义了4个顶点,然后需要创建索引缓冲对象:

unsigned int EBO;
glGenBuffers(1, &EBO);

然后绑定EBO,并把索引复制到缓冲里,类型设置为GL_ELEMENT_ARRAY_BUFFER

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

最后使用glDrawElements替换glDrawArrays函数,即从索引缓冲渲染。第一个参数指定了绘制的模式;第二个参数指定了绘制顶点的数量;第三个参数是索引的类型,这里是GL_UNSIGNED_INT;最后一个参数指定EBO中的偏移量。

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。即在每次使用索引渲染时需要绑定相应的EBO,不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象,绑定VAO时会自定绑定EBO。


当目标是GL_ELEMENT_ARRAY_BUFFER时,VAO会存储glBindBuffer的函数调用,即也会存储解绑调用,所以确保不能在解绑VAO之前解绑EBO,否则会没有EBO的配置了。

最后的绘制代码如下:

// ..:: 初始化代码 :: ..
// 1. 绑定顶点数组对象
glBindVertexArray(VAO);
// 2. 把我们的顶点数组复制到一个顶点缓冲中,供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. 复制我们的索引数组到一个索引缓冲中,供OpenGL使用
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);[...]// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

【OpenGL基础】|| OpenGL渲染过程介绍相关推荐

  1. 【OpenGL基础】|| 着色器介绍

    文章目录 1. GLSL 2. 数据类型 3. 输入与输出 4. uniform变量 5. 更多属性 着色器(shader)是运行在GPU上的小程序,将输入转化为输出.着色器之间不能相互通信,唯一的沟 ...

  2. 【OpenGL】计算机图形学OpenGL基础

    前面几节是跟着B站视频学习的OpenGL基础操作,算是熟悉了一下环境,下面进入学习opengl中文教程网站,开始真正进入OpenGL的大门. 下面开始学习计算机图形编程,收获自己做出很酷的东西的喜悦. ...

  3. OpenGL基础33:帧缓冲(上)之离屏渲染

    在之前的章节,所有的物体都是中规中矩的显示的,只考虑了光照对物体的影响,那假设想要显示特殊的效果该怎么操作呢?例如马赛克风.将所有的物体都显示为黑白色,就像上世纪80年代的灰白电视一样,又或者说将整个 ...

  4. OpenGL基础知识介绍和简单使用

    OpenGL基础知识介绍 OpenGL简介 OpenGL 专业词解析 1.OpenGL上下文[context] 2.渲染 3.顶点数组和顶点缓冲区 4.着色器程序Shader 5.顶点着色器(Vert ...

  5. SDL2源码分析之OpenGL ES在windows上的渲染过程

    SDL2源码分析之OpenGL ES在windows上的渲染过程 更新于2018年11月4日. 更新于2018年11月21日. ffmpeg + SDL2实现的简易播放器 ffmpeg和SDL非常强大 ...

  6. OpenGL开发库的详细介绍

    OpenGL开发库的组成 开发基于OpenGL的应用程序,必须先了解OpenGL的库函数.它采用C语言风格,提供大量的函数来进行图形的处理和显示.OpenGL库函数的命名方式非常有规律.所有OpenG ...

  7. OpenGL基础43:抗锯齿

    一.走样与反走样 走样(Aliasing)就是锯齿化,反走样(Anti-aliasing)就是抗锯齿 只要玩过游戏,那么都应该对抗锯齿不陌生,不少游戏也都有关于抗锯齿的设置 如上图,放大的部分能很明显 ...

  8. OpenGL基础4:最基础的单元 —— 三角形

    在刚学 OpenGL 的时候千万不要沉迷细节,后面你会慢慢懂,又或者说:放弃 前置:OpenGL基础1:最简单的OpenGL例子 一.最简单的着色器 前面讲过,如果我们打算做渲染的话必需要配置顶点和片 ...

  9. OpenGL基础2:OpenGL简介

    小朋友你是否有很多问号,不知道为什么需要这么多复杂的方法,也不知道为什么要这么设计,更不知道仅仅显示一个点都那么复杂,后面3D游戏中的那么精美的表现是否离自己很远很远--要不,再来一起看看openGL ...

最新文章

  1. 零基础入门学习Python(31)-异常处理2-try语句
  2. 【图像处理】透视变换 Perspective Transformation
  3. 防护很重要!教你教你认识和检验安防产品的IP防护等级
  4. windows下nginx+tomcat分布式集群部署
  5. Travelling (三进制+状压dp)
  6. mysql 命令手册_MySQL 常用命令手册
  7. 开源跳板机(堡垒机)Jumpserver v2.0.0 部署篇
  8. java演练 谁在哪里做什么 文字小游戏开发
  9. 大数据之-Hadoop完全分布式_集群时间同步---大数据之hadoop工作笔记0043
  10. 【leetcode】91. Decode Ways A-Z的字母表示1-26的数字,反向破解多少种字符串的可能性...
  11. 多渠道归因分析:互联网的归因江湖(二)
  12. 机器学习_一条会说666的咸鱼
  13. 手写分页sql_MyBatis-Plus 分页查询以及自定义sql分页的实现
  14. opencv-6 边缘检测(Prewitt算子,Sobel算子,Laplacian算子)
  15. 【心得感想】谈谈工作效率及沟通
  16. 如何区分两列中不同数据_快速找出Excel表格中两列数据不同内容的3种方法!
  17. Vim光标定位操作快捷键
  18. unity 制作行李箱密码
  19. 33 | 如何做好验收测试?
  20. 六大行业动向,给2021年新能源汽车行业画下句点

热门文章

  1. 【无标题】申请测绘资质的详细流程及所需详细资料有哪些?
  2. pandas数据类型转为str
  3. 常用JS效果 需要时更新。。。
  4. 如何给PDF文件去水印,10秒轻松搞定
  5. 【git 报错】git add添加到暂存区报错:fatal: pathspec ‘xxx‘ did not match any files
  6. linux命令画圣诞树图片,在Linux终端下显示动画的圣诞树
  7. pdf书籍资源共享_书籍和更多内容已获许可使用知识共享
  8. git commit使用其他人的用户名和邮箱提交代码
  9. NDK/C++ 耗时统计类TimeUtils
  10. 制作小地图MiniMap小结