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


前言

什么是 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_PRESS
    std::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)都设为3
    glfwWindowHint(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 context
    glfwMakeContextCurrent(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轴):

扫描二维码关注公众号,回复: 15056276 查看本文章

在这里插入图片描述

绘制三角形的第一步是以数组的形式传递 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:
    // 着色器程序ID
    unsigned 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();
        // 转换数据流到string
        vertexCode   = 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_PRESS
    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)都设为3
    glfwWindowHint(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 context
    glfwMakeContextCurrent(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 program
    Shader ourShader("3.3.shader.vs", "3.3.shader.fs"); // you can name your shader files however you like
 
    unsigned 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 position
    gl_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 position
    gl_Position = vec4(aPos.x + xOffset, aPos.y, aPos.z, 1.0); 
    ourColor = aColor;
}

猜你喜欢

转载自blog.csdn.net/Jormungand_V/article/details/125846382