本文整理自LearnOpenGL 及LearnOpenGL CN ,后者为前者的中文版, 完整学习的话建议前往原网站。
三个复杂的单词
顶点数组对象:Vertex Array Object,VAO
顶点缓冲对象:Vertex Buffer Object,VBO
索引缓冲对象:Element Buffer Object,EBO或Index Buffer Object,IBO
OpenGL描述的都是3D事物(2D事物会将其z轴坐标设置为0),但却要显示在2D的屏幕和窗口上,导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素。
3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。图形渲染管线可以被划分为两个主要部分:第一部分把你的3D坐标转换为2D坐标,第二部分是把2D坐标转变为实际的有颜色的像素。
2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。
图形渲染管线接受一组3D坐标,然后把它们转变为你屏幕上的有色2D像素输出。图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。正是由于它们具有并行执行的特性,当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)。
图形渲染管线的每个阶段的抽象展示,蓝色部分代表的是我们可以注入自定义的着色器的部分。
以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形,这个数组叫做顶点数据(Vertex Data);顶点数据是一系列顶点的集合。由顶点属性(Vertex Attribute)表示,顶点属性可以包含3d位置,颜色等。
为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的渲染类型(一系列点、三角形?),这些提示叫做
图元
(Primitive):GL_POINTS、GL_TRIANGLES、GL_LINE_STRIP
图形渲染管线第一
部分:顶点着色器
(Vertex Shader),输入为一个单独的顶点(3D坐标),输出为另一种3D坐标。
图元装配阶段
顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状(输出顶点)。
几何着色器
:输入为一系列顶点,可以通过产生新顶点生成其他形状。
光栅化阶段(Rasterization Stage)
,把图元映射为最终屏幕上相应的像素
生成供片段着色器
(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切
(Clipping)。裁切会丢弃超出你的视图(视口)以外的所有像素,用来提升执行效率。
OpenGL中的一个片段是OpenGL渲染一个
像素
所需的所有数据。
片段着色器
:计算一个像素的最终颜色(光照、阴影、光的颜色).
Alpha测试和混合(Blending)阶段
:检测片段的深度和模板(Stencil)值,如果在其他物体后面就会被丢弃。检查alpha(透明度)进行混合(混合(Blend))。
输入数据后,OpenGL将数据进行标准化,OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。
定义一个三角形,指定三个顶点,以标准化的形式给出,
GLfloat vertices[] = {
-0.5f, -0.5f, 0.0f, //2d三角形的z轴坐标设置为0
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
从顶点着色器中出来的坐标就应该是标准设备坐标了([-1,1])。
标准化设备坐标接着会变换为屏幕空间坐标(Screen-space Coordinates),这是使用你通过glViewport函数提供的数据,进行视口变换(Viewport Transform)完成的。所得的屏幕空间坐标又会被变换为片段(标准坐标)输入到片段着色器中。
定义这样的顶点数据以后,我们会把它作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器,顶点着色器会分配内存存储这些顶点数据,还要配置OpenGL如何解释这些内存,并且指定其如何发送给显卡。
顶点缓冲对象(Vertex Buffer Objects, VBO)
负责管理这个内存,我们可以使用VBO一次性发送一大批数据到显卡上(对比glBegin(),glEnd()),而不是每个顶点发送一次。从CPU(电脑内存)把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存(显存)中后,顶点着色器几乎能立即访问顶点,速度很快。
VBO有一个独一无二的对象(唯一的ID)。
生成VBO对象
GLuint VBO;
glGenBuffers(1, &VBO);
确定或绑定它的类型
glBindBuffer(GL_ARRAY_BUFFER, VBO);
从这一刻起,我们使用的任何
(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定
的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前
定义的顶点数据复制到缓冲的内存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
参数依次为目标缓冲类型,传输数据大小,实际数据,显卡管理数据的方式。
GL_STATIC_DRAW :数据不会或几乎不会改变。
GL_DYNAMIC_DRAW:数据会被改变很多。
GL_STREAM_DRAW :数据每次绘制时都会改变。
指定以便于显卡是否把数据分配在可以高速读写的区域。
可编程的着色器,
#version 330 core //版本
layout (location = 0) in vec3 position; //输入变量的位置值,输入顶点属性
void main()
{
gl_Position = vec4(position.x, position.y, position.z, 1.0); //预定义的变量,用于输出,
}
创建着色器
GLuint vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER); //类型,顶点着色器
将源码附加到对象上,编译
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader); //1为着色器源码字符串数量
检测是否编译成功
GLint success;
GLchar 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;
}
计算像素最后的颜色输出。
源码
#version 330 core
out vec4 color; //输出变量
void main()
{
color = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
编译着色器
GLuint fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, null);
glCompileShader(fragmentShader);
然后将两个着色器对象链接到着色器程序中。
着色器程序对象:多个着色器合并之后并最终链接完成的版本。
创建程序对象
GLuint shaderProgram;
shaderProgram = glCreateProgram(); //返回ID引用
附加着色器到程序对象,并链接。
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
检测程序是否失败:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
...
}
激活该程序对象。
glUseProgram(shaderProgram);
并删除着色器
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
下面是如何解释内存中的数据。
顶点数据内存分配:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
参数解释
顶点属性,将数据传递到顶点着色器中layout(location=0)
的顶点属性中。
大小。
数据类型。
如果标准化,如果标准化,所有数据都会被映射到[0,1]或[-1,1]。
步长,连续的顶点数据之间的步长。
位置数据在缓冲起始位置的偏移量。顶点属性从VBO获取数据,通过调用
glVetexAttribPointer
决定使用哪个VBO。
绘制物体的代码例子
// 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(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 2. 当我们渲染一个物体时要使用着色器程序
glUseProgram(shaderProgram);
// 3. 绘制物体
someOpenGLFunctionThatDrawsOurTriangle();
顶点数组对象(Vertex Array Object, VAO),类似于VBO,配置不同的属性只需要切换绑定不同的VAO。
VAO存储的内容:
glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
通过glVertexAttribPointer设置的顶点属性配置。
通过glVertexAttribPointer调用与顶点属性关联的顶点缓冲对象。
VAO创建:
GLuint VAO;
glGenVertexArrays(1, &VAO);
// ..:: 初始化代码(只运行一次 (除非你的物体频繁改变)) :: ..
// 1. 绑定VAO
glBindVertexArray(VAO);
// 2. 把顶点数组复制到缓冲中供OpenGL使用
glBindBuffer(GL_ARRAY_BUFFER, VBO); //绑定类型为2D
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); //真正的数据内容
// 3. 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0); //默认不启用
//4. 解绑VAO
glBindVertexArray(0);
...
// ..:: 绘制代(游戏循环中) :: ..
// 5. 绘制物体
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();
glBindVertexArray(0);
绘制图元
glUseProgram(shaderProgram); //使用激活的着色器
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3); //三角形,索引,顶点个数
glBindVertexArray(0); //解绑
索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。
存储索引,OpenGL根据索引决定绘制哪个顶点(顺序)。
使用方法类似于VBO,绑定->复制到缓冲。
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //注意target
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
使用glDrawElements
代替glDrawArrays
指明从索引缓冲渲染,
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);//绘制模式,顶点数量,索引的类型,EBO的偏移量。
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO,顶点数组对象
同样可以保存索引缓冲对象
的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。
当目标是GL_ELEMENT_ARRAY_BUFFER的时候,VAO会储存glBindBuffer的函数调用。这也意味着它也会储存解绑调用,所以确保你没有在解绑VAO之前解绑索引数组缓冲,否则它就没有这个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);
// 3. 设定顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
glEnableVertexAttribArray(0);
// 4. 解绑VAO(不是EBO!)
glBindVertexArray(0);
[...]你好三角形
// ..:: 绘制代码(游戏循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);