OpenGL | 通过绘制一个三角形来入门 OpenGL 图形渲染管线

文章目录

  • 前言
    • 什么是 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 只是一种规范,严格意义上来讲不能视为库,不同的显卡生产商在 OpenGLAPI 提供上有着细微的差距,而 OpenGL 的核心代码和显卡核心技术绑定因此是非开源的,使用时通常仅能对厂商提供的 API 进行操作。
  • OpenGL 优势在于它是跨平台的,一份代码可以在 MacWindowsLinux,甚至移动端的 iOSAndroid 上运行。(比为不同平台专门编写不同 APIDirect3D 更适合懒人,当然在 iOS 上可能更多还是选择苹果专用的 Metal)。
  • 众所周知,用编程语言(C++、Java、C#)实现的程序都是运行在 CPU 上,但实现图形处理的时候,为了精确控制 GPU,因此需要将代码从 CPU 上移植到 GPU 上(代码在 GPU 上的运行速度会更快),而着色器允许我们在 GPU 上写代码,是否有 可编程着色器(programmable shaders)modern OpenGLlegacy OpenGL 的主要区别,当然,本质上是 现代OpenGL老OpenGL 让渡了更多的 控制权 给程序员。
  • OpenGL Context(OpenGL 上下文环境) 的创建需要借助一些工具,比如轻量级的库 GLFW(Graphics Library Framework,图形库框架),GLFW 的主要功能是 创建并管理窗口 和 OpenGL 上下文,同时还提供了基础权限操作——处理手柄、键盘、鼠标输入的功能。

回顾

在上一篇博客中,最后检测配置 OpenGL 环境是否成功是通过一段代码来实现的,代码中有关 GLFW 和 GLAD 的内容比较简单,在此不做赘述。可以通过代码中的注释进行理解,觉得注释没有讲清楚的也可以通过 LearnOpenGL CN 中的 你好,窗口 一文进行学习。

本篇博客全部代码。代码中有关 GLFWGLAD 的内容如下,这些代码类似于模块一般,几乎是我们要渲染图像并显示在窗口时必须编写的:

#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像素 的处理过程。该过程可以分为两个阶段:

  1. 3D坐标转换为2D坐标;
  2. 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像素,它只会处理标准化设备坐标【在顶点着色器中处理过的顶点坐标就是标准化设备坐标】 ,标准化设备坐标是一个 xyz 值在 -1.0 ~ 1.0 的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在屏幕上。下面是一个定义在标准化设备坐标中的三角形(忽略z轴):

在这里插入图片描述

绘制三角形的第一步是以数组的形式传递 33D坐标 作为图形渲染管线的输入,用来表示一个三角形。

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

  1. 顶点数据会被作为输入发送给图形渲染管线的第一个处理阶段:顶点着色器。
  2. 顶点着色器会在GPU上创建内存用于储存顶点数据。
  3. 顶点缓冲对象(VBO)负责管理这个GPU内存(通常被称为显存),他在显存中储存大量顶点,配置OpenGL如何解释这些内存,并且指定显存中的数据如何发送给显卡。

CPU把数据发送到显卡相对较慢,但VBO可以一次性发送一大批数据,而不是每个顶点发送一次。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点。

接下来,执行如下流程:

glGenBuffers

  1. 使用 glGenBuffers 函数和一个缓冲 ID 生成一个 VBO 对象:
unsigned int VBO;
glGenBuffers(1, &VBO);

函数原型:

void glGenBuffers(GLsizei n,GLuint * buffers);
  • n:生成的缓冲对象的数量;
  • buffers:用来存储缓冲对象名称的数组。
  • 此时仅生成了一个缓冲对象,但是缓冲对象的类型还不确定。

glBindBuffer

  1. 使用 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)绑定了多个缓冲对象(VBOVBO1VBO2),此时将数据传入哪个缓冲对象就成了问题。

glBufferData

  1. 调用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_DRAWGL_STREAM_DRAW 将导致显卡把数据放在能够高速写入的内存部分,而内存空间是十分宝贵的,不要随便使用,因此合理填写 usage 对应的值,不会/几乎不会改变的数据一定要填写为 GL_STATIC_DRAW


建立了一个顶点和一个片段着色器

着色器是什么?

我们发现上图中某些阶段会用到着色器(Shader),它是运行在 GPU 上的可编程的小程序,在图形渲染管线某个特定部分快速处理数据。着色器有以下特点:

  • 运行在 GPU 上,节省了宝贵的 CPU 时间。
  • 着色器只是一种把 输入 处理后 输出 的程序。除输入/输出之外不能相互通信。

为什么需要使用着色器?

实际上,图像处理完全可以在 CPU 中进行,通过串行多核计算实现图形渲染。但渲染工作都十分单一,仅仅是多点计算,对于处理复杂工作的 CPU 而言,将大量时间花在处理简单的渲染工作上无疑是种资源的浪费。

于是 GPU 这个专注于图像处理的硬件诞生了,它是一个允许并行计算的超多核处理器,GPU 拥有成百上千个核心,意味着在图形处理方面 GPU 能带来更快的处理速度和更好的图形效果。(常见 CPU 可能是4核的,但是两者核心能处理的工作复杂度是不可相提并论的)

综上,OpenGL 实现了一种可以让点和像素的计算在 GPU 中进行的规范,这就是着色器。

这里仅对着色器做简单的认知介绍,更多关于着色器的知识可详见该文。


着色器的结构

着色器通常具有以下结构:

  1. 声明版本
  2. 输入和输出变量
  3. Uniform
  4. 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坐标,但 xyz 的取值范围不再是整个空间,而是 -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:包含 3float 分量的三维向量。
  • aPos 是一个 vec3 输入变量。
  • gl_Position:预定义的变量,类型为 vec4,这里通过 vec3 变量 aPos 的数据来充当 vec4 构造器的参数,把 w 分量设置为 1.0fvec.w 分量不是用作表达空间中的位置的(我们处理的是 3D 不是 4D),而是用在 透视除法(Perspective Division) 上。

为了能够让 OpenGL 使用顶点着色器,必须在运行时动态编译它的源代码。

  1. 将顶点着色器的源代码硬编码在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";
  1. glCreateShader 创建这个着色器对象,通过 unsigned int 存储 glCreateShader 返回的 ID ,以便于引用该着色器对象所在的内存空间。
// glCreateShader函数参数:要创建的着色器类型
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
  1. 将着色器源码附加到着色器对象上,然后编译它:
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource 的参数:

  • GLuint shader:指定要被编译源代码的着色器对象的句柄(ID)
  • GLsizei count:指定传递的源码字符串数量,这里只有一个
  • const GLchar **string:指向包含源代码的字符串的指针数组,这也就是为什么上面的代码在调用时传入的是 vertexShaderSource 指针本身的地址,而不是指针指向的字符串的地址。(因为该参数会被二次解引用)
  • const GLint *length:为 NULL 则将整个字符串进行拷贝替换;不为 NULL 则将替换指定长度部分。
  1. 通过 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,就会得到混合色——黄色。

  1. GLSL 片段着色器源代码,声明输出变量:
#version 330 core
out vec4 FragColor;void main()
{FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 
  • out :关键字,声明输出变量到 FragColor 中。
  1. 硬编码
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";
  1. 创建着色器对象并记录ID、附加源码、编译:
// 与顶点着色器的最大区别 glCreateShader 的参数 —— GL_FRAGMENT_SHADER
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
  1. 检查编译是否成功,代码与顶点着色器检查部分相同。
  2. 把两个着色器对象链接到一个用来渲染的着色器程序(Shader Program)中。

着色器程序对象

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终完成链接的版本。

当链接着色器至一个程序的时候,程序会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

  1. 创建一个程序对象,shaderProgram 接收新创建程序对象的ID引用:
unsigned int shaderProgram = glCreateProgram();
  1. 把之前编译完成顶点/片段着色器附加到着色器程序对象上,然后用 glLinkProgram 链接:
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
  1. 如检测编译时是否成功那样,检测链接是否成功:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
  1. 在把着色器对象链接到程序对象以后,删除着色器对象,不再需要它们了,释放占用的内存:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
  1. 激活着色器程序对象
// 将这段函数加入while循环函数的渲染部分,就可以激活这个着色器程序对象了。
glUseProgram(shaderProgram);

把顶点数据链接到顶点着色器的顶点属性上

顶点缓冲数据会被解析为下面这样子:
在这里插入图片描述

  • 位置数据被储存为32位(4字节)浮点值。
  • 每个位置包含3个这样的值。
  • 在这3个值之间没有空隙(或其他值)。这几个值在数组中紧密排列。
  • 数据中第一个值在缓冲开始的位置。

解析顶点数据对应的代码实现:

  1. 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_BYTEGL_UNSIGNED_BYTEGL_SHORTGL_UNSIGNED_SHORTGL_FIXEDGL_FLOAT,初始值为 GL_FLOAT。此外,GLSLvec* 都是由浮点数值组成的。
  • normalized:指定当被访问时,固定点数据值是否应该被归一化【Normalize】(GL_TRUE)或者直接转换为固定点值(GL_FALSE)。如果为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间。
  • stride:指定连续顶点属性之间的偏移量。初始值为0,意为顶点属性是紧密排列在一起的。由于下个组位置数据在 3float 之后,我们把步长设置为3 * sizeof(float)。在此例中两个顶点属性之间没有空隙,因此也可以设置为 0 来让 OpenGL 决定具体步长是多少(只有当数值是紧密排列时才可用)。
  • pointer:指定第一个组件在数组的第一个顶点属性中的偏移量。该数组与GL_ARRAY_BUFFER绑定,储存于缓冲区中。初始值为0;由于位置数据在数组的开头,所以偏移量是0

每个顶点属性从一个顶点缓冲对象管理的内存中获得它的数据,而具体是从哪个顶点缓冲对象(程序中可以有多个顶点缓冲对象)获取则是通过在调用glVertexAttribPointer时绑定到GL_ARRAY_BUFFER的缓冲对象决定的。同一时刻只能有一个缓冲对象绑定到GL_ARRAY_BUFFER,此时绑定到GL_ARRAY_BUFFER的是先前定义的VBO,顶点属性0会链接到它的顶点数据。

  1. glEnableVertexAttribArray 启用顶点属性。顶点属性默认是禁用的。
glEnableVertexAttribArray(0);

函数原型:

void glEnableVertexAttribArray(GLuint index);void glDisableVertexAttribArray(GLuint index);void glEnableVertexArrayAttrib(	GLuint vaobj, GLuint index);void glDisableVertexArrayAttrib(GLuint vaobj, GLuint index);
  • vaobj:指定 glDisableVertexArrayAttribglEnableVertexArrayAttrib 函数的顶点数组对象(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,而是很多的时候,就不得不多次调用 glBufferDataglVertexAttribPointerglEnableVertexAttribArray。绑定正确的缓冲对象、为每个物体配置所有顶点属性很快就变成一件麻烦事。

这就需要一个能够存储状态配置的对象,然后通过绑定这个对象来恢复状态。这就要靠VAO了。


顶点数组对象 VAO

为什么要使用VAO?

VBO大幅提升了绘制效率,但是顶点的位置坐标、法向量、纹理坐标等不同方面的数据每次使用时需要单独指定,重复了一些不必要的工作。

而顶点数组对象(VAO)可以像VBO那样被绑定,当配置顶点属性指针时,你只需要调用将glVertexAttribPointerglEnableVertexAttribArray一次,之后再绘制物体的时候只需要绑定顶点属性指针相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。

OpenGL的核心模式要求我们使用VAO。如果绑定VAO失败,OpenGL会拒绝绘制任何东西。

VAO储存的内容

  • glEnableVertexAttribArrayglDisableVertexAttribArray 的调用。
  • 通过 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 的流程:

  1. 创建一个VAO,在VAO后创建的VBO都属于该VAO
unsigned int VAO;
glGenVertexArrays(1, &VAO);
  1. 绑定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 顶点数据

  1. 首先,要定义(不重复的)顶点,和绘制出矩形所需的索引:
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

  1. 创建元素缓冲对象:
unsigned int EBO;
glGenBuffers(1, &EBO);

glBufferData

  1. 先绑定EBO然后用glBufferData把索引复制到缓冲:
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

glPolygonMode

  1. 控制多边形的显示方式,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

  1. 用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
  • *indicesEBO的偏移量。不再使用索引缓冲对象的时候可以传递一个索引数组。

VAO 与 EBO

glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取其索引。这意味着每次使用索引渲染对象时都必须绑定相应的EBO,这有点麻烦。有没有感觉到这个问题很眼熟?这不就是没有VAO只有VBO时碰到的问题么。更巧的是VAO也跟踪EBO绑定。在绑定VAO时,之前绑定的最后一个EBO自动存储为VAOEBO

在这里插入图片描述

上图有一个小细节: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中的着色器发送数据的方式。它的特性如下:

  1. uniform全局的,意味着uniform变量必须在每个着色器程序对象中都是独一无二的,而且它可以被着色器程序的任意着色器在任意阶段访问。
  2. 无论把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的类型。可能的后缀有:
在这里插入图片描述

在上面的例子中,由于需要分别设定uniform4float值,所以通过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;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/443670.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

javascript特效_如何在网页添加鼠标点击特效

经常有同学问我怎么做到的&#xff0c;本论坛属于DZ当然用的是插件啦。偶然在网上找到一个关于wordpress的特效代码&#xff0c;分享给大家。WordPress 添加鼠标点击特效实际上这种教程在网上搜索一下有一大堆&#xff0c;已经是各大博主玩烂的东西了。不过既然给自己的博客加上…

android nio debug模式正常 release包crash_Flutter包大小治理上的探索与实践

Flutter作为一种全新的响应式、跨平台、高性能的移动开发框架&#xff0c;在性能、稳定性和多端体验一致上都有着较好的表现&#xff0c;自开源以来&#xff0c;已经受到越来越多开发者的喜爱。但是&#xff0c;Flutter的引入往往带来包体积的增大&#xff0c;给很多研发团队带…

sam格式的结构和意义_各种格式的练字本,对写字真有帮助吗

图片来源于笔势通各种格式的练字本现在越来越多&#xff0c;目的主要是便于学生把握好笔画的位置和布局&#xff0c;从而把整个字的结构处理好&#xff0c;常见的有米字格&#xff0c;回宫格等。这些练字本对于初学者来说肯定是有帮助的&#xff0c;特别是低年级学生。当然随着…

硬件结构图_那曲地表水电子除垢仪结构图

那曲地表水电子除垢仪结构图水处理设备也应断电停止使用&#xff0c;系统长期停止运行或季节性停止运行&#xff0c;在系统停止运行前&#xff0c;在水中投加适量缓蚀剂&#xff0c;并采取满水湿保护的措施&#xff0c;以减小腐蚀&#xff0c;保护系统。开启进水阀检查无误后电…

双屏怎么快速切换鼠标_在笔记本上实现双屏设计,怎么做到的?

如果给你的笔记本电脑安装两个屏幕&#xff0c;你会用来干什么&#xff1f;是上班时间主屏幕放着PPT&#xff0c;副屏幕偷摸玩游戏&#xff1b;还是主屏幕玩游戏&#xff0c;副屏幕刷刷B站视频&#xff1b;亦或是主屏幕P着图&#xff0c;副屏幕在网上找找能用的素材&#xff1f…

信元模式mpls 避免环路_【基础】交换机堆叠模式

堆叠是指将一台以上的交换机组合起来共同工作&#xff0c;以便在有限的空间内提供尽可能多的端口。多台交换机经过堆叠形成一个堆叠单元。可堆叠的交换机性能指标中有一个"最大可堆叠数"的参数&#xff0c;它是指一个堆叠单元中所能堆叠的最大交换机数&#xff0c;代…

为什么叫日上_古雷150万吨乙烯,为啥叫芒果项目?

古雷150万吨乙烯&#xff0c;为啥叫芒果项目&#xff1f;福建石油化工集团有限责任公司9月1日在福州举行的一场新闻通气会上透露&#xff0c;石化基地引进世界化工巨头——沙特基础工业公司(简称SABIC)&#xff0c;合资合作共建中沙古雷乙烯项目。中沙古雷乙烯项目将在福建古雷…

Linux学习:第四章-vi编辑器

一vi编辑器简介vim全屏幕纯文本编辑器别名alias命令‘命令别名’ aliasvi’vim’ alias lsls --colorttyls正常显示颜色 alias lsls --colornever 环境变量配置文件/root/.bashrc 二vim使用 1vi模式 vi文件名 命令模式 输入模式 末行模式 命令----》输入a&#xff1a;追加i&…

gradient设置上下渐变_PaintCode Mac使用教程:如何使用渐变色

Mac平台上一款强大的iOS矢量绘图编程软件PaintCode Mac&#xff0c;无论您是程序员还是设计师&#xff0c;paintcode3能够让你像在PS中画图一样绘制各种UI图形&#xff0c;而且paintcode3会自动帮你生成针对MacOS X或iOS平台Objective-C或C#代码&#xff0c;能够节约大量的编程…

opencv计算图像亮度调节_OpenCV教程创建Trackbar图像对比度、亮度值调整

这篇文章中我们一起学习了如何在OpenCV中用createTrackbar函数创建和使用轨迹条&#xff0c;以及图像对比度、亮度值的动态调整。文章首先详细讲解了OpenCV2.0中的新版创建轨迹条的函数createTrackbar&#xff0c;并给上一个详细注释的示例。然后讲解图像的对比度、亮度值调整的…

find linux 目录深度_浪里淘沙,详解Linux系统中Find命令的实用技巧

知了小巷&#xff1a;浪里淘沙&#xff0c;详解Linux系统中Find命令的实用技巧。啊哈&#xff0c;找到了&#xff01;当我们需要在Linux系统上定位某个文件或目录时&#xff0c;find命令通常是必备之选。它使用起来非常简单&#xff0c;但有许多不同的可选项&#xff0c;允许我…

剑指offer之从上到下打印二叉树

从上到下打印出二叉树的每个节点&#xff0c;同一层的节点按照从左到右的顺序打印。 例如: 给定二叉树: [3,9,20,null,null,15,7], 返回&#xff1a; [3,9,20,15,7] 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;https://leetcode-cn.com/problem…

小米真蓝牙耳机说明书_小米真无线蓝牙耳机Air2 SE评测:仅需169元,享受随心畅听体验...

继小米真无线蓝牙耳机Air 2、小米真无线蓝牙耳机Air 2S之后&#xff0c;小米公司又于2020年5月19日再次推出了一款售价更为亲民的真无线蓝牙耳机新品——小米真无线蓝牙耳机Air2 SE&#xff0c;该机不仅延续了小米真无线蓝牙耳机Air 2系列的外观设计&#xff0c;支持开盒弹窗、…

三点外接圆_故地重游伪切圆——伪外接圆的基本性质

在思考一个有关于伪外接圆的等角线问题时&#xff0c;我回想起伪外接圆的一道小题目&#xff0c;这是2012年罗马尼亚大师杯的第六题&#xff0c;这道题目直接以结论的形式呈现出了伪外接圆的基本性质&#xff0c;是一道入门伪外接圆必做的精巧小题。当然有些读者可能从未见过&q…

一点等于多少厘米_马桶知识介绍,你了解马桶多少

我们可能并不了解我们经常运用的马桶&#xff0c;认为马桶便是简简单单的规划&#xff0c;没什么技术含量。其实不然&#xff0c;马桶的规划也包含了不少物理学原理。假如你家里的马桶出现毛病&#xff0c;首先要排查毛病的原因&#xff0c;但是假如不了解马桶结构图那就很难把…

1151压力变送器型号_日本进口横河EJA530E压力变送器型号解读!

横河EJA变送器对大家来说也许不陌生&#xff0c;但是对于EJA变送器的型号很多人还不是很懂&#xff0c;因为一个全型号代表这很多参数&#xff0c;每一个字母和每一个数字背后都是一个准确的参数&#xff0c;我们在选型的时候要提供必要的参数&#xff0c;更具参数选出合适的型…

怎样在数组末尾添加数据_如何利用C++实现可变长的数组?

应该执行什么功能&#xff1f;假设我们要实现一个将自动扩展的数组类&#xff0c;是否需要实现函数&#xff1f;让我们从下面主要功能使用的功能开始&#xff0c;看看我们需要实现哪些功能。输出结果&#xff1a;0 1 2 3 40 1 2 100 4您需要做什么才能实现上述功能&#xff1f;…

覆盖索引与联合索引_浅析MySQL的索引覆盖和索引下推

写在前面在MySQL数据库中&#xff0c;索引对查询的速度有着至关重要的影响&#xff0c;理解索引也是进行数据库性能调优的起点&#xff0c;索引就是为了提高数据查询的效率。今天我们来聊聊在MySQL索引优化中两种常见的方式&#xff0c;索引覆盖和索引下推索引覆盖要了解索引覆…

循环斐波那契数列_第五课:斐波那契数列(第一课时)

简介&#xff1a;又称黄金分割数列、因数学家列昂纳多斐波那契以兔子繁殖为例子而引入&#xff0c;故又称为“兔子数列”&#xff0c;指的是这样一个数列&#xff1a;1、1、2、3、5、8、13、21、34……在数学上&#xff0c;斐波那契数列以如下被以递推的方法定义&#xff1a;F(…

命令行 蓝牙_Ubuntu使用BlueZ驱动蓝牙dongle

蓝牙dongle即蓝牙适配器&#xff0c;一般为USB接口&#xff0c;通过USB连接到PC等设备。连接dongle后&#xff0c;PC即可使用驱动程序控制dongle连接其它蓝牙设备。本文主要介绍在Ubuntu系统中安装BlueZ的方法及蓝牙的使用。01获取BlueZBlueZ是Linux系统的官方蓝牙协议栈&#…