文章目录
- 前言
- 什么是 OpenGl ?
- 回顾
- openGL 的 Object
- 显存结构
- 工作阶段
- 通过顶点缓冲对象将顶点数据初始化至缓冲中
- 标准化设备坐标
- 顶点缓冲对象 VBO
- glGenBuffers
- glBindBuffer
- glBufferData
- 建立了一个顶点和一个片段着色器
- 着色器是什么?
- 为什么需要使用着色器?
- 着色器的结构
- 顶点着色器
- 片段着色器
- 着色器程序对象
- 把顶点数据链接到顶点着色器的顶点属性上
- 绘制单个物体
- 顶点数组对象 VAO
- 为什么要使用VAO?
- VAO储存的内容
- 使用 VAO 的流程:
- 绘制三角形(glDrawArrays函数)
- 元素缓冲对象 EBO / 索引缓冲对象 IBO
- vertices 顶点数据
- glGenBuffers
- glBufferData
- glPolygonMode
- glDrawElements
- VAO 与 EBO
- 绘制矩形
- uniform
- glGetUniformLocation
- glUniform4f
- 向VAO加入颜色数据
- 颜色数据存入VAO
- 顶点着色器和片段着色器的联动
- 绘制三色三角形
- 封装一个着色器类
- 构造函数
- use函数
- uniform的set函数
- 使用
- 拓展
- 让三角形颠倒
- 移动三角形
前言
什么是 OpenGl ?
- OpenGL 只是一种规范,严格意义上来讲不能视为库,不同的显卡生产商在 OpenGL 的 API 提供上有着细微的差距,而 OpenGL 的核心代码和显卡核心技术绑定因此是非开源的,使用时通常仅能对厂商提供的 API 进行操作。
- OpenGL 优势在于它是跨平台的,一份代码可以在 Mac、Windows、Linux,甚至移动端的 iOS、Android 上运行。(比为不同平台专门编写不同 API 的 Direct3D 更适合懒人,当然在 iOS 上可能更多还是选择苹果专用的 Metal)。
- 众所周知,用编程语言(C++、Java、C#)实现的程序都是运行在 CPU 上,但实现图形处理的时候,为了精确控制 GPU,因此需要将代码从 CPU 上移植到 GPU 上(代码在 GPU 上的运行速度会更快),而着色器允许我们在 GPU 上写代码,是否有 可编程着色器(programmable shaders) 是
modern OpenGL
和legacy OpenGL
的主要区别,当然,本质上是 现代OpenGL 比 老OpenGL 让渡了更多的 控制权 给程序员。 - OpenGL Context(OpenGL 上下文环境) 的创建需要借助一些工具,比如轻量级的库 GLFW(Graphics Library Framework,图形库框架),GLFW 的主要功能是 创建并管理窗口 和 OpenGL 上下文,同时还提供了基础权限操作——处理手柄、键盘、鼠标输入的功能。
回顾
在上一篇博客中,最后检测配置 OpenGL 环境是否成功是通过一段代码来实现的,代码中有关 GLFW 和 GLAD 的内容比较简单,在此不做赘述。可以通过代码中的注释进行理解,觉得注释没有讲清楚的也可以通过 LearnOpenGL CN 中的 你好,窗口 一文进行学习。
本篇博客全部代码。代码中有关 GLFW 和 GLAD 的内容如下,这些代码类似于模块一般,几乎是我们要渲染图像并显示在窗口时必须编写的:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>// 对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用。
// 参数:window - 被改变大小的窗口,width、height-窗口的新维度。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// 告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport)// 这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标// 调用glViewport函数来设置窗口的维度(Dimension)// 前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)glViewport(0, 0, width, height);
}// 实现输入控制的函数:查询GLFW是否在此帧中按下/释放相关键,并做出相应反应
void processInput(GLFWwindow *window)
{// glfwGetKey两个参数:窗口,按键// 没有被按下返回 GLFW_PRESSstd::cout << "是否点击ESC?" << std::endl;std::cout << glfwGetKey(window, GLFW_KEY_ESCAPE) << std::endl;if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)// 被按下则将 WindowShouldClose 属性置为 true// 以便于在关闭 渲染循环glfwSetWindowShouldClose(window, true);
}const unsigned int SCR_WIDTH = 800; // 创建窗口的宽
const unsigned int SCR_HEIGHT = 600; // 创建窗口的高int main()
{glfwInit(); // 初始化GLFW// glfwWindowHint函数的第一个参数代表选项的名称// 第二个参数接受一个整型,用来设置这个选项的值// 将主版本号(Major)和次版本号(Minor)都设为3glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);// 使用的是核心模式(Core-profile)glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);#ifdef __APPLE__// macOS需要本语句生效 glfwWindow 的相关配置glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif// 参数依次为:宽、高、窗口的名称,显示器用于全屏模式,设为NULL是为窗口// 窗口的上下文为共享资源,NULL为不共享资源GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "FirstWindow", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 释放空间,防止内存溢出glfwTerminate();return -1;}// 创建完毕之后,需要让window的context成为当前线程的current contextglfwMakeContextCurrent(window);// 窗口大小改变时视口也要随之改变,这通过对窗口注册 framebuffer_size_callback 实现。// 它会在每次窗口大小被调整时调用glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glfwGetProcAddress是glfw提供的用来 加载系统相关的OpenGL函数指针地址 的函数// gladLoadGLLoader函数根据使用者的系统定义了正确的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}/* 渲染循环(Render Loop) */// glfwWindowShouldClose 检查一次GLFW是否被要求退出// 为true时渲染循环结束while(!glfwWindowShouldClose(window)){// 监测键盘输入processInput(window);/* 渲染 */// glfwSwapBuffers 交换颜色缓冲,用来绘制并作为输出显示在屏幕glfwSwapBuffers(window);// glfwPollEvents 检查是否有触发事件glfwPollEvents();}glfwTerminate();return 0;
}
openGL 的 Object
为什么 OpenGL 会有这么多的 Object?
因为 OpenGL 的改进目标就是把以前所有直接从客户端传值到服务端的操作,都变成对(服务端中)显存的Object的更新,之后客户端只要绑定(Bind)
一下,服务端就能直接在显存读取Object
的数据了,如此一来就避免了大量低效的数据传输。而每个Object
就对应一个显存结构。
显存结构
- 顶点数组对象:Vertex Array Object【VAO】
- 顶点缓冲对象:Vertex Buffer Object【VBO】
- 元素缓冲对象:Element Buffer Object【EBO】
- 索引缓冲对象:Index Buffer Object【IBO】
工作阶段
图形渲染管线(Graphics Pipeline),指的是一堆原始图形数据途经一个输送管道,将 OpenGL 中的 3D坐标 转为 适配屏幕 的 2D像素 的处理过程。该过程可以分为两个阶段:
- 3D坐标转换为2D坐标;
- 2D坐标转变为有颜色的2D像素。
2D坐标和像素不同,2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到屏幕/窗口分辨率的限制。
下图是图形渲染管线的每个阶段的抽象展示,蓝色部分表示该阶段可以注入自定义着色器:
图形渲染管线本质上是一个状态机,每个阶段将会把前一个阶段的输出作为输入,且这些阶段都允许并行执行。
- 顶点着色器见下文。
图元装配(Primitive Assembly)
阶段将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS
,那么就是一个顶点),并所有的点装配成指定图元的形状;上图例子中是一个三角形。- 几何着色器把图元装配阶段输出的一系列顶点的集合作为输入,可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。上图例子中生成了另一个三角形。
光栅化阶段(Rasterization Stage)
把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)
使用的片段(Fragment)
(一个片段是 OpenGL 渲染一个像素所需的所有数据)。在片段着色器运行之前会执行裁切(Clipping)
。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。- 片段着色器见下文。
Alpha测试
和混合(Blending)
阶段检测片段的对应的深度
和模板(Stencil))值
,用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(物体的透明度)
并对物体进行混合(Blend)
。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
图形渲染管线非常复杂,包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器通常使用它默认的着色器就行了。
通过顶点缓冲对象将顶点数据初始化至缓冲中
标准化设备坐标
OpenGL 不是简单地把所有的 3D坐标
变换为屏幕上的 2D像素
,它只会处理标准化设备坐标【在顶点着色器中处理过的顶点坐标就是标准化设备坐标】 ,标准化设备坐标是一个 x
、y
和 z
值在 -1.0 ~ 1.0
的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在屏幕上。下面是一个定义在标准化设备坐标中的三角形(忽略z轴):
绘制三角形的第一步是以数组的形式传递 3
个 3D坐标
作为图形渲染管线的输入,用来表示一个三角形。
PS:在真实的程序里输入数据通常都不是标准化设备坐标,而是顶点坐标,将顶点坐标转换为标准化设备坐标是在顶点着色器中完成的,但是本篇博客旨在尽可能简洁的阐述完渲染流程,因此直接向顶点着色器传入标准化设备坐标。
以标准化设备坐标的形式(OpenGL的可见区域)定义一个顶点数据数组:
float vertices[] = {-0.5f, -0.5f, 0.0f,0.5f, -0.5f, 0.0f,0.0f, 0.5f, 0.0f
};
vertices
中每一行就是一个顶点(Vertex):一个 3D坐标 的数据的集合。vertices
叫做顶点数据(Vertex Data):是一系列顶点的集合。
由于 OpenGL 是在3D空间中工作的,而渲染的是一个2D三角形,因此将顶点的z
坐标设置为 0.0
。这样子的话三角形每一点的 深度(Depth) 都是一样的,从而使它看上去像是2D的。
深度: 代表一个像素在空间中和屏幕的距离,如果离屏幕远就可能被别的像素遮挡而变得不可见,因此会被丢弃以节省资源。
顶点缓冲对象 VBO
- 顶点数据会被作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
- 顶点着色器会在
GPU
上创建内存用于储存顶点数据。 - 顶点缓冲对象(
VBO
)负责管理这个GPU
内存(通常被称为显存),他在显存中储存大量顶点,配置OpenGL如何解释这些内存,并且指定显存中的数据如何发送给显卡。
从
CPU
把数据发送到显卡相对较慢,但VBO
可以一次性发送一大批数据,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点。
接下来,执行如下流程:
glGenBuffers
- 使用
glGenBuffers
函数和一个缓冲ID
生成一个VBO
对象:
unsigned int VBO;
glGenBuffers(1, &VBO);
函数原型:
void glGenBuffers(GLsizei n,GLuint * buffers);
n
:生成的缓冲对象的数量;buffers
:用来存储缓冲对象名称的数组。- 此时仅生成了一个缓冲对象,但是缓冲对象的类型还不确定。
glBindBuffer
- 使用
glBindBuffer()
来确定生成的缓冲对象的类型,顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER
:
glBindBuffer(GL_ARRAY_BUFFER, VBO);
函数原型:
void glBindBuffer(GLenum target,GLuint buffer);
target
:缓冲对象的类型;buffer
:要绑定的缓冲对象的名称。
官方文档指出:GL_INVALID_VALUE is generated if buffer is not a name previously returned form a call to glGenBuffers。
换句话说,buffer
虽然是 GLuint
类型的,但是不能直接指定个常量比如说 2
,如果这样做了,就会出现 GL_INVALID_VALUE
的错误:
OpenGL允许我们同时绑定多个缓冲类型,但要求这些缓冲类型是不同的。举个简单例子:
我要把数据存入顶点缓冲区,但是顶点缓冲区(GL_ARRAY_BUFFER
)绑定了多个缓冲对象(VBO
、VBO1
、VBO2
),此时将数据传入哪个缓冲对象就成了问题。
glBufferData
- 调用
glBufferData
函数,把之前定义的顶点数据复制到缓冲的内存中:
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
函数原型:
void glBufferData(GLenum target,GLsizeiptr size,const GLvoid * data,GLenum usage);
target
:目标缓冲的类型。size
:指定传输数据的大小(以字节为单位);用sizeof
计算出顶点数据大小就行。data
:指定要复制数据的内存地址,如果不复制数据,则为NULL
。usage
:指定显卡管理给定的数据的形式。有三种:GL_STATIC_DRAW
:数据不会或几乎不会改变。GL_DYNAMIC_DRAW
:数据会被改变很多。GL_STREAM_DRAW
:数据每次绘制时都会改变。
GL_DYNAMIC_DRAW
和 GL_STREAM_DRAW
将导致显卡把数据放在能够高速写入的内存部分,而内存空间是十分宝贵的,不要随便使用,因此合理填写 usage
对应的值,不会/几乎不会改变的数据一定要填写为 GL_STATIC_DRAW
。
建立了一个顶点和一个片段着色器
着色器是什么?
我们发现上图中某些阶段会用到着色器(Shader),它是运行在 GPU
上的可编程的小程序,在图形渲染管线某个特定部分快速处理数据。着色器有以下特点:
- 运行在
GPU
上,节省了宝贵的CPU
时间。 - 着色器只是一种把 输入 处理后 输出 的程序。除输入/输出之外不能相互通信。
为什么需要使用着色器?
实际上,图像处理完全可以在 CPU
中进行,通过串行多核计算实现图形渲染。但渲染工作都十分单一,仅仅是多点计算,对于处理复杂工作的 CPU
而言,将大量时间花在处理简单的渲染工作上无疑是种资源的浪费。
于是 GPU
这个专注于图像处理的硬件诞生了,它是一个允许并行计算的超多核处理器,GPU
拥有成百上千个核心,意味着在图形处理方面 GPU
能带来更快的处理速度和更好的图形效果。(常见 CPU
可能是4核的,但是两者核心能处理的工作复杂度是不可相提并论的)
综上,OpenGL 实现了一种可以让点和像素的计算在 GPU
中进行的规范,这就是着色器。
这里仅对着色器做简单的认知介绍,更多关于着色器的知识可详见该文。
着色器的结构
着色器通常具有以下结构:
- 声明版本
- 输入和输出变量
- Uniform
- main函数。每个着色器的入口点都是main函数,在这里处理所有的输入变量,并将结果输出到输出变量中。
// 声明版本
#version version_number
// 输入变量
in type in_variable_name;
in type in_variable_name;
// 输出变量
out type out_variable_name;
// uniform
uniform type uniform_name;
// main 函数
int main(){// 处理输入并进行一些图形操作...// 输出处理过的结果到输出变量out_variable_name = weird_stuff_we_processed;
}
顶点着色器
因为 GPU
中没有默认的顶点/片段着色器,所以现代 OpenGL
要求至少设置一个顶点着色器和一个片段着色器才能实现渲染。
顶点着色器主要功能是把 3D坐标 转换为 标准化设备坐标,后者依然是 3D坐标,但 x
、y
、z
的取值范围不再是整个空间,而是 -1.0 ~ 1.0
。同时允许我们对顶点属性进行一些基本处理。
用 着色器语言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
:GLSL 版本声明,OpenGL 3.3 以及更高版本中,GLSL版本号和OpenGL的版本是匹配的(比如说GLSL 420版本对应于OpenGL 4.2)。core
:核心模式。layout (location = 0)
:设定了输入变量的位置值(Location),位置数据是一种顶点属性。in
:关键字,在着色器中声明所有的输入顶点属性(Input Vertex Attribute) 到aPos
中。vec3
:包含3
个float
分量的三维向量。aPos
是一个vec3
输入变量。gl_Position
:预定义的变量,类型为vec4
,这里通过vec3
变量aPos
的数据来充当vec4
构造器的参数,把w
分量设置为1.0f
,vec.w
分量不是用作表达空间中的位置的(我们处理的是 3D 不是 4D),而是用在 透视除法(Perspective Division) 上。
为了能够让 OpenGL
使用顶点着色器,必须在运行时动态编译它的源代码。
- 将顶点着色器的源代码硬编码在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";
- 用
glCreateShader
创建这个着色器对象,通过unsigned int
存储glCreateShader
返回的ID
,以便于引用该着色器对象所在的内存空间。
// glCreateShader函数参数:要创建的着色器类型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
- 将着色器源码附加到着色器对象上,然后编译它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
glShaderSource
的参数:
GLuint shader
:指定要被编译源代码的着色器对象的句柄(ID)GLsizei count
:指定传递的源码字符串数量,这里只有一个const GLchar **string
:指向包含源代码的字符串的指针数组,这也就是为什么上面的代码在调用时传入的是vertexShaderSource
指针本身的地址,而不是指针指向的字符串的地址。(因为该参数会被二次解引用)const GLint *length
:为 NULL 则将整个字符串进行拷贝替换;不为 NULL 则将替换指定长度部分。
- 通过
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;
}
glGetShaderiv
的原型:
void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
shader
:指定要查询的着色器对象ID。pname
:指定检查内容,可接受的符号名称有:GL_SHADER_TYPE
:用来判断并返回着色器类型,顶点着色器返回GL_VERTEX_SHADER
,片元着色器返回GL_FRAGMENT_SHADER
。GL_DELETE_STATUS
:判断着色器是否被删除,是返回GL_TRUE
,否则返回GL_FALSE
。GL_COMPILE_STATUS
:用于检测编译是否成功,成功为GL_TRUE
,否则为GL_FALSE
。GL_INFO_LOG_LENGTH
:用于返回着色器的信息日志的长度,包括空终止字符(即存储信息日志所需的字符缓冲区的大小)。 如果着色器没有信息日志,则返回0
。GL_SHADER_SOURCE_LENGTH
:返回着色器源码长度,不存在则返回0。
params
:因为根据第二个参数值的不同,返回的结果会有很多种,所以单独存储在输入的第三个参数中。这也是为什么函数返回值是void
而不是GLuint
。
片段着色器
片段着色器(Fragment Shader)
可以接收由光栅化阶段生成的每个片段数据、纹理数据、3D场景的数据(比如光照、阴影、光的颜色等),用来计算出每个光栅化空白像素的最终颜色。是所有OpenGL高级效果产生的地方。
RGBA:红色、绿色、蓝色和 alpha(透明度) 分量,当在 OpenGL 或 GLSL 中定义一个颜色的时候,把颜色每个分量的强度设置在 0.0 到 1.0 之间。比如设置红为 1.0f,绿为 1.0f,就会得到混合色——黄色。
- GLSL 片段着色器源代码,声明输出变量:
#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
out
:关键字,声明输出变量到FragColor
中。
- 硬编码
const char *fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""void main()\n""{\n"" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n""}\n\0";
- 创建着色器对象并记录ID、附加源码、编译:
// 与顶点着色器的最大区别 glCreateShader 的参数 —— GL_FRAGMENT_SHADER
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
- 检查编译是否成功,代码与顶点着色器检查部分相同。
- 把两个着色器对象链接到一个用来渲染的着色器程序(
Shader Program
)中。
着色器程序对象
着色器程序对象(Shader Program Object
)是多个着色器合并之后并最终完成链接的版本。
当链接着色器至一个程序的时候,程序会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。
- 创建一个程序对象,
shaderProgram
接收新创建程序对象的ID引用:
unsigned int shaderProgram = glCreateProgram();
- 把之前编译完成的顶点/片段着色器附加到着色器程序对象上,然后用
glLinkProgram
链接:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
- 如检测编译时是否成功那样,检测链接是否成功:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
- 在把着色器对象链接到程序对象以后,删除着色器对象,不再需要它们了,释放占用的内存:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
- 激活着色器程序对象:
// 将这段函数加入while循环函数的渲染部分,就可以激活这个着色器程序对象了。
glUseProgram(shaderProgram);
把顶点数据链接到顶点着色器的顶点属性上
顶点缓冲数据会被解析为下面这样子:
- 位置数据被储存为32位(4字节)浮点值。
- 每个位置包含3个这样的值。
- 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列。
- 数据中第一个值在缓冲开始的位置。
解析顶点数据对应的代码实现:
glVertexAttribPointer
指定了渲染时索引值为index
的顶点属性数组的数据:
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
函数原型:
void glVertexAttribPointer( GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride,const GLvoid * pointer);
index
:要配置的顶点属性的索引值。在顶点着色器中,曾使用layout(location = 0)
定义了position
顶点属性的位置值(Location
)。location
的值0
就是索引值。size
:指定每个顶点属性的组件数量,必须为1、2、3或者4。初始值为4。(如position
是由3个组件[x,y,z]组成,而颜色是4个组件[r,g,b,a])。type
:指定数组中每个组件的数据类型。可用的符号常量有:GL_BYTE
、GL_UNSIGNED_BYTE
、GL_SHORT
、GL_UNSIGNED_SHORT
、GL_FIXED
和GL_FLOAT
,初始值为GL_FLOAT
。此外,GLSL
中vec*
都是由浮点数值组成的。normalized
:指定当被访问时,固定点数据值是否应该被归一化【Normalize】(GL_TRUE
)或者直接转换为固定点值(GL_FALSE
)。如果为GL_TRUE
,所有数据都会被映射到0
(对于有符号型signed
数据是-1
)到1
之间。stride
:指定连续顶点属性之间的偏移量。初始值为0
,意为顶点属性是紧密排列在一起的。由于下个组位置数据在3
个float
之后,我们把步长设置为3 * sizeof(float)
。在此例中两个顶点属性之间没有空隙,因此也可以设置为0
来让OpenGL
决定具体步长是多少(只有当数值是紧密排列时才可用)。pointer
:指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER
绑定,储存于缓冲区中。初始值为0
;由于位置数据在数组的开头,所以偏移量是0
。
每个顶点属性从一个顶点缓冲对象管理的内存中获得它的数据,而具体是从哪个顶点缓冲对象(程序中可以有多个顶点缓冲对象)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的缓冲对象决定的。同一时刻只能有一个缓冲对象绑定到GL_ARRAY_BUFFER,此时绑定到GL_ARRAY_BUFFER的是先前定义的VBO,顶点属性0会链接到它的顶点数据。
glEnableVertexAttribArray
启用顶点属性。顶点属性默认是禁用的。
glEnableVertexAttribArray(0);
函数原型:
void glEnableVertexAttribArray(GLuint index);void glDisableVertexAttribArray(GLuint index);void glEnableVertexArrayAttrib( GLuint vaobj, GLuint index);void glDisableVertexArrayAttrib(GLuint vaobj, GLuint index);
vaobj
:指定glDisableVertexArrayAttrib
和glEnableVertexArrayAttrib
函数的顶点数组对象(VAO)的名称。index
:指定 启用/禁用 的索引(顶点属性位置值)。
绘制单个物体
到此所有流程就结束了,如果想在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();
当顶点属性个数不再是 1
,而是很多的时候,就不得不多次调用 glBufferData
和 glVertexAttribPointer
、 glEnableVertexAttribArray
。绑定正确的缓冲对象、为每个物体配置所有顶点属性很快就变成一件麻烦事。
这就需要一个能够存储状态配置的对象,然后通过绑定这个对象来恢复状态。这就要靠VAO
了。
顶点数组对象 VAO
为什么要使用VAO?
VBO
大幅提升了绘制效率,但是顶点的位置坐标、法向量、纹理坐标等不同方面的数据每次使用时需要单独指定,重复了一些不必要的工作。
而顶点数组对象(VAO)可以像VBO
那样被绑定,当配置顶点属性指针时,你只需要调用将glVertexAttribPointer
、glEnableVertexAttribArray
一次,之后再绘制物体的时候只需要绑定顶点属性指针相应的VAO
就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO
就行了。
OpenGL的核心模式要求我们使用VAO。如果绑定VAO失败,OpenGL会拒绝绘制任何东西。
VAO储存的内容
glEnableVertexAttribArray
和glDisableVertexAttribArray
的调用。- 通过
glVertexAttribPointer
设置的顶点属性配置。 - 通过
glVertexAttribPointer
调用与顶点属性关联的VBO
。
- VAO 中的
attribute pointer
(属性指针)指向 VBO 中的某个属性(pos
【位置】或者col
【颜色】),如上图就是attribute pointer 0
来管理位置属性,attribute pointer 1
来管理颜色属性。 - 对于 VBO 来讲,每个顶点 的 所有属性 都相邻存储,顶点0的位置(
pos[0]
)、颜色(col[0]
),因此每一种attribute pointer
都会有 步长(stride
)。
使用 VAO 的流程:
- 创建一个VAO,在VAO后创建的VBO都属于该VAO。
unsigned int VAO;
glGenVertexArrays(1, &VAO);
- 绑定VAO,绑定成功后应该绑定和配置对应的VBO和属性指针,之后解绑VAO供再次使用。
// 绑定VAO
glBindVertexArray(VAO);
打算绘制多个物体时,首先要生成/配置所有的VAO(和必须的VBO及属性指针),然后储存它们供后面使用。绘制其中一个物体的时候就拿出相应的VAO,绑定它,绘制完物体后,再解绑VAO。
绘制三角形(glDrawArrays函数)
使用 glDrawArrays
函数,通过当前激活的着色器、之前定义的顶点属性配置和VBO的顶点数据(通过VAO间接绑定)来绘制图元:
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
函数原型:
GL_APICALL void GL_APIENTRY glDrawArrays (GLenum mode, GLint first, GLsizei count);
mode
:绘制方式,可选值有:GL_POINTS
:把每一个顶点作为一个点进行处理。GL_LINES
:连接每两个顶点作为一个独立的线段,N个顶点总共绘制N/2条线段。GL_LINE_STRIP
:绘制从第一个顶点到最后一个顶点依次相连的一组线段。GL_LINE_LOOP
:在GL_LINE_STRIP
的基础上,最后一个顶点和第一个顶点相连。GL_TRIANGLES
:把每三个顶点作为一个独立的三角形。GL_TRIANGLE_STRIP
:绘制一组相连的三角形。GL_TRIANGLE_FAN
:围绕第一个点绘制相连的三角形,第一个顶点作为所有三角形的顶点。
first
:从数组缓存中的哪一位开始绘制,一般为0。count
:数组中顶点的数量。
绘制三角形的全部代码详见。
元素缓冲对象 EBO / 索引缓冲对象 IBO
元素缓冲对象 EBO / 索引缓冲对象 IBO 是同一个东西。假设想要绘制一个矩形,可以通过绘制两个三角形来组成一个矩形(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
个顶点,这样就产生50%
的额外开销。好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样只要储存4
个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。这便是 元素缓冲区对象(EBO) 的工作方式。
EBO
存储要绘制的顶点的索引,即索引绘制(Indexed Drawing)。使用EBO
的流程如下:
vertices 顶点数据
- 首先,要定义(不重复的)顶点,和绘制出矩形所需的索引:
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,2,3)就是顶点数组vertices的下标,// 这样可以由下标代表顶点组合成矩形0, 1, 3, // 第一个三角形1, 2, 3 // 第二个三角形
};
glGenBuffers
- 创建元素缓冲对象:
unsigned int EBO;
glGenBuffers(1, &EBO);
glBufferData
- 先绑定EBO然后用glBufferData把索引复制到缓冲:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glPolygonMode
- 控制多边形的显示方式,GL_LINE以线框模式绘制。
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
函数原型:
void glPolygonMode(GLenum face,GLenum mode);
- face:确定显示模式将适用于物体的哪些部分,控制多边形的正面和背面的绘图模式:
- GL_FRONT表示显示模式将适用于物体的前向面(也就是物体能看到的面)
- GL_BACK表示显示模式将适用于物体的后向面(也就是物体上不能看到的面)
- GL_FRONT_AND_BACK表示显示模式将适用于物体的所有面
- mode:确定选中的物体的面以何种方式显示(显示模式):
- GL_POINT表示显示顶点,多边形用点显示
- GL_LINE表示显示线段,多边形用轮廓显示
- GL_FILL表示显示面,多边形采用填充形式
glDrawElements
- 用glDrawElements来替换glDrawArrays函数,表示从索引缓冲区使用当前绑定的索引缓冲对象中的索引进行绘制:
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
函数原型:
void glDrawElements(GLenum mode,GLsizei count,GLenum type,const GLvoid *indices);
mode
:绘制方式,同glDrawArrays
中的mode
参数。count
:打算绘制顶点的个数,vertices中有两个顶点被复用了,因此这里填6
。type
:索引的类型,一般都是GL_UNSIGNED_INT
。*indices
:EBO
的偏移量。不再使用索引缓冲对象的时候可以传递一个索引数组。
VAO 与 EBO
glDrawElements
函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER
目标的EBO
中获取其索引。这意味着每次使用索引渲染对象时都必须绑定相应的EBO
,这有点麻烦。有没有感觉到这个问题很眼熟?这不就是没有VAO
只有VBO
时碰到的问题么。更巧的是VAO
也跟踪EBO
绑定。在绑定VAO
时,之前绑定的最后一个EBO
自动存储为VAO
的EBO
。
上图有一个小细节:VAO
只可以绑定一个EBO
,但是可以绑定多个VBO
。所以确保先解绑EBO
再解绑VAO
,否则VAO
就没有EBO
配置了,也就无法成功绘制了。
OpenGL VAO VBO EBO(IBO)的绑定、解绑问题值得一看。
绘制矩形
最后的初始化和绘制代码现在看起来像这样:
// ..:: 初始化代码 :: ..
// 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);[...]glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);// ..:: 绘制代码(渲染循环中) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO); // 如果绘制多个对象,在这里切换绑定VAO
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
绘制矩形的全部代码详见。
uniform
Uniform
是除顶点属性外另一种从CPU
中的应用向GPU
中的着色器发送数据的方式。它的特性如下:
uniform
是全局的,意味着uniform
变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。- 无论把
uniform
值设置成什么,uniform
会一直保存这些数据,直到旧有数据被重置或更新。
举个例子,通过uniform
设置三角形的颜色:
#version 330 core
out vec4 FragColor;
uniform vec4 ourColor; // 在OpenGL程序代码中设定这个变量
void main(){FragColor = ourColor;
}
如果你声明了一个 uniform
却在 GLSL
代码中没用过,编译器会静默移除这个变量,导致最后编译出的版本中并不会包含它,这可能导致几个非常麻烦的错误!
在给uniform
添加数据之前,首先需要找到着色器中uniform
属性的索引/位置值。uniform
是种类似于in
的输入数据,之前layout (location = 0) in vec3 aPos
是通过索引值location = 0
将外部数据绑定的,而uniform
完全不需要layout
,而是通过着色器程序对象和uniform
的名字:
/* 在循环渲染的代码块中加入下列代码 */
// 获取运行的秒数
float timeValue = glfwGetTime();
// 通过sin函数让颜色在0.0到1.0之间改变,最后将结果储存到greenValue里。
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
// 通过glGetUniformLocation查询uniform ourColor的位置值,返回-1代表没有找到。
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
// 通过glUniform4f函数设置uniform值
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);
注意,查询uniform
地址不要求之前使用过着色器程序(调用glUseProgram
),但是更新一个uniform
(调用glUniform4f
)之前必须先使用程序,因为设置uniform
是在当前激活的着色器程序中进行的。
glGetUniformLocation
GLint glGetUniformLocation(GLuint program, const GLchar *name);
program
:指定要查询的着色器程序对象。name
:指向一个没有终止符的字符串,其中包含要查询位置的uniform
变量的名称。
glUniform4f
OpenGL
其核心是一个C
库,所以不支持类型重载,在函数参数不同的时候就要为其定义新的函数;glUniform
是一个典型例子。这个函数有一个特定的后缀,标识设定的uniform
的类型。可能的后缀有:
在上面的例子中,由于需要分别设定uniform
的4
个float
值,所以通过glUniform4f
传递数据(也可以使用glUniformfv
版本)。
绘制变色三角形的代码可以参考这里。
向VAO加入颜色数据
颜色数据存入VAO
颜色属于顶点属性的一种,因此它也可以存入VAO。试试将颜色数据存入VAO然后传给顶点着色器,而不是传给片段着色器。
float vertices[] = {// 位置 // 颜色0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 红色-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 绿色0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 蓝色
};
由于现在有更多的数据要发送到顶点着色器,因此有必要去调整一下顶点着色器,使它能够接收颜色值作为一个顶点属性输入。用layout
标识符来把aColor
属性的位置值设置为1
:
const char *vertexShaderSource = "#version 330 core\n""layout (location = 0) in vec3 point;\n""layout (location = 1) in vec3 color;\n""out vec3 ourColor;\n""void main()\n""{\n"" gl_Position = vec4(point.x, point.y, point.z, 1.0);\n"" ourColor = color;\n""}\0";
添加了新的顶点属性(颜色),就需要更新VBO
的内存并重新配置顶点属性指针。更新后的VBO
内存中的数据如下图所示:
知道了现在使用的布局,就可以使用glVertexAttribPointer
函数更新顶点格式:
// 位置
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // 启用layout 0// 颜色
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));
glEnableVertexAttribArray(1); // 启用layout 1
顶点着色器和片段着色器的联动
此时不再使用uniform
来传递片段的颜色了,而是以顶点着色器的输出ourColor
作为片段着色器的输入:
const char *fragmentShaderSource = "#version 330 core\n""out vec4 FragColor;\n""in vec3 ourColor;\n""void main()\n""{\n"" FragColor = vec4(ourColor, 1.0f);\n""}\n\0";
绘制三色三角形
程序运行结果如下:
咦?运行结果貌似和设计预期有所区别。按理来说应该是一个红绿蓝纯三色的三角形,怎么在三个角颜色还蛮纯正的,越接近三角形中心颜色越混杂呢?这是因为片段着色器中进行了片段插值。
当渲染一个三角形时,光栅化阶段通常会将几何着色器划分好的区域细分成更多的片段。光栅会根据每个片段在三角形上所处位置进行插值。比如有一个线段,上面的端点是绿色的,下面的端点是蓝色的。如果一个片段着色器在线段的70%
(靠近绿色端)的位置运行,它的颜色输入属性就是30%
蓝 + 70%
绿。
上图有3个顶点和相应的3个颜色,从这个三角形的像素来看它可能包含50000左右的片段,片段着色器为这些像素进行插值颜色。如果你仔细看这些颜色就应该能明白了:红先变成紫再变为蓝色。片段插值会被应用到片段着色器的所有输入属性上。
封装一个着色器类
编写、编译、管理着色器是件麻烦事。不妨写一个类从硬盘读取着色器,然后编译并链接它们,并对它们进行错误检测。来吧,将目前所学知识封装到一个抽象对象中!
把着色器类全部放在在头文件里,以方便移植。先添加必要的include,并定义类结构:
#ifndef SHADER_H
#define SHADER_H#include <glad/glad.h>; // 包含glad来获取所有的必须OpenGL头文件#include <string>
#include <fstream>
#include <sstream>
#include <iostream>class Shader{
public:// 着色器程序IDunsigned int ID;// 构造函数从文件路径读取顶点/片段着色器源代码以构建着色器Shader(const char* vertexPath, const char* fragmentPath);// 使用/激活程序void use();// uniform工具函数void setBool(const std::string &name, bool value) const; void setInt(const std::string &name, int value) const; void setFloat(const std::string &name, float value) const;
private:// 用于检查着色器编译/链接错误的实用程序函数void checkCompileErrors(unsigned int index, std::string type){int success;char infoLog[1024];if (type != "PROGRAM"){glGetShaderiv(index, GL_COMPILE_STATUS, &success);if (!success){glGetShaderInfoLog(index, 1024, NULL, infoLog);std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << std::endl;}}else{glGetProgramiv(index, GL_LINK_STATUS, &success);if (!success){glGetProgramInfoLog(index, 1024, NULL, infoLog);std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << std::endl;}}}
};#endif
构造函数
Shader(const char* vertexPath, const char* fragmentPath){// 1. 从文件路径中获取顶点/片段着色器std::string vertexCode;std::string fragmentCode;std::ifstream vShaderFile;std::ifstream fShaderFile;// 保证ifstream对象可以抛出异常:vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);try{// 打开文件vShaderFile.open(vertexPath);fShaderFile.open(fragmentPath);std::stringstream vShaderStream, fShaderStream;// 读取文件的缓冲内容到数据流中vShaderStream << vShaderFile.rdbuf();fShaderStream << fShaderFile.rdbuf();// 关闭文件流vShaderFile.close();fShaderFile.close();// 转换数据流到stringvertexCode = vShaderStream.str();fragmentCode = fShaderStream.str();}catch(std::ifstream::failure e){std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;}const char* vShaderCode = vertexCode.c_str();const char* fShaderCode = fragmentCode.c_str();// 2. 编译着色器unsigned int vertex, fragment;// 顶点着色器vertex = glCreateShader(GL_VERTEX_SHADER);glShaderSource(vertex, 1, &vShaderCode, NULL);glCompileShader(vertex);checkCompileErrors(vertex, "VERTEX");// 片段着色器也类似fragment = glCreateShader(GL_FRAGMENT_SHADER);glShaderSource(fragment, 1, &fShaderCode, NULL);glCompileShader(fragment);checkCompileErrors(fragment, "FRAGMENT");// 着色器程序ID = glCreateProgram();glAttachShader(ID, vertex);glAttachShader(ID, fragment);glLinkProgram(ID);checkCompileErrors(ID, "PROGRAM");// 删除着色器,它们已经链接到我们的程序中了,已经不再需要了glDeleteShader(vertex);glDeleteShader(fragment);
}
use函数
void use()
{ glUseProgram(ID);
}
uniform的set函数
void setBool(const std::string &name, bool value) const{glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}
void setInt(const std::string &name, int value) const{ glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}
void setFloat(const std::string &name, float value) const{ glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}
使用
引入Shader.h
头文件来简化代码,以三色三角形的代码为例:
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <iostream>
#include <cmath>
#include "Shader.h"using namespace std;// 对窗口注册一个回调函数(Callback Function),它会在每次窗口大小被调整的时候被调用。
// 参数:window - 被改变大小的窗口,width、height-窗口的新维度。
void framebuffer_size_callback(GLFWwindow* window, int width, int height)
{// 告诉OpenGL渲染窗口的尺寸大小,即视口(Viewport)// 这样OpenGL才只能知道怎样根据窗口大小显示数据和坐标// 调用glViewport函数来设置窗口的维度(Dimension)// 前两个参数控制窗口左下角的位置。第三个和第四个参数控制渲染窗口的宽度和高度(像素)glViewport(0, 0, width, height);
}// 实现输入控制的函数:查询GLFW是否在此帧中按下/释放相关键,并做出相应反应
void processInput(GLFWwindow *window)
{// glfwGetKey两个参数:窗口,按键// 没有被按下返回 GLFW_PRESSif(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)// 被按下则将 WindowShouldClose 属性置为 true// 以便于在关闭 渲染循环glfwSetWindowShouldClose(window, true);
}const unsigned int SCR_WIDTH = 800; // 创建窗口的宽
const unsigned int SCR_HEIGHT = 600; // 创建窗口的高int main(){glfwInit(); // 初始化GLFW// glfwWindowHint函数的第一个参数代表选项的名称// 第二个参数接受一个整型,用来设置这个选项的值// 将主版本号(Major)和次版本号(Minor)都设为3glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);// 使用的是核心模式(Core-profile)glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);#ifdef __APPLE__// macOS需要本语句生效 glfwWindow 的相关配置glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
#endif// 参数依次为:宽、高、窗口的名称,显示器用于全屏模式,设为NULL是为窗口// 窗口的上下文为共享资源,NULL为不共享资源GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "FirstWindow", NULL, NULL);if (window == NULL){std::cout << "Failed to create GLFW window" << std::endl;// 释放空间,防止内存溢出glfwTerminate();return -1;}// 创建完毕之后,需要让window的context成为当前线程的current contextglfwMakeContextCurrent(window);// 窗口大小改变时视口也要随之改变,这通过对窗口注册 framebuffer_size_callback 实现。// 它会在每次窗口大小被调整时调用glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);// glfwGetProcAddress是glfw提供的用来 加载系统相关的OpenGL函数指针地址 的函数// gladLoadGLLoader函数根据使用者的系统定义了正确的函数if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)){std::cout << "Failed to initialize GLAD" << std::endl;return -1;}/* 设置顶点数据(和缓冲区)并配置顶点属性 */float vertices[] = {// 位置 // 颜色0.5f, -0.5f, 0.0f, 1.0f, 0.0f, 0.0f, // 右下 红色-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, 0.0f, // 左下 绿色0.0f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f // 顶部 蓝色};// build and compile our shader programShader ourShader("3.3.shader.vs", "3.3.shader.fs"); // you can name your shader files however you likeunsigned int VBOs[2], VAOs[2];glGenVertexArrays(2, VAOs);glGenBuffers(2, VBOs); // 生成2个 VBO 对象/* 首先绑定顶点数组对象,然后绑定并设置顶点缓冲区,然后配置顶点属性。 */glBindVertexArray(VAOs[0]);glBindBuffer(GL_ARRAY_BUFFER, VBOs[0]); // 确定生成的缓冲对象的类型// 把顶点数据复制到缓冲的内存中glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);// layout(location=0), 每个顶点的pos属性(vec*)由3个组件构成,//(vec*)中的值的类型为GL_FLOAT, 转换为固定点值, 第一个组件的偏移量为0// 位置glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);glEnableVertexAttribArray(0); // 启用layout 0// 颜色glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float)));glEnableVertexAttribArray(1); // 启用layout 1/* 渲染循环(Render Loop) */// glfwWindowShouldClose 检查一次GLFW是否被要求退出// 为true时渲染循环结束while(!glfwWindowShouldClose(window)){// 监测键盘输入processInput(window);/* 渲染 */// 状态设置函数,设置清空屏幕所用的颜色glClearColor(0.2f, 0.3f, 0.3f, 1.0f);// 状态使用函数,使用设定好的颜色来清除旧的颜色缓冲glClear(GL_COLOR_BUFFER_BIT);// 上面两种函数起到的作用也可以用 glClearBufferfv 来现实/*GLfloat color[] = {0.2, 0.3, 0.3, 1.0};glClearBufferfv(GL_COLOR, 0, color);*/ourShader.use();glBindVertexArray(VAOs[0]);glDrawArrays(GL_TRIANGLES, 0, 3);// glfwSwapBuffers 交换颜色缓冲,用来绘制并作为输出显示在屏幕glfwSwapBuffers(window);// glfwPollEvents 检查是否有触发事件glfwPollEvents();}// 可选:一旦所有资源超出其用途,则取消分配:glDeleteVertexArrays(2, VAOs);glDeleteBuffers(2, VBOs);glfwTerminate();return 0;
}
拓展
让三角形颠倒
让一个三角形颠倒,除了修改顶点数组,还能想到什么办法?修改顶点着色器源代码!
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;void main(){// just add a - to the y positiongl_Position = vec4(aPos.x, -aPos.y, aPos.z, 1.0); ourColor = aColor;
}
移动三角形
使用uniform
定义一个水平偏移量,在顶点着色器中使用该偏移量就可以实现三角形的移动。
// In Render Loop of your CPP file :
float offset = 0.5f;
ourShader.setFloat("xOffset", offset);// In your vertex shader code file:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aColor;out vec3 ourColor;uniform float xOffset;void main()
{// add the xOffset to the x position of the vertex positiongl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0); ourColor = aColor;
}