文章目录
- 1.1 什么是opengl
- openg程序基本所需执行的操作
- 1.2 初识opengl程序
- 基础图形学名词
- 第三方库
- 1.3 opengl语法
- 数据类型
- 1.4 渲染管线rendering pipeline
- 准备向opengl传输数据(缓存初始化)
- 将数据传输到opengl
- 顶点着色
- 细分着色(几何图元数量增加,模型外观变平顺)
- 几何着色
- 图元装配
- 剪切
- 光栅化
- 片元着色
- 逐片元的操作(混合与测试)
- 1.5 第一个程序(绘制三角形) / 配置环境
- main函数——创建一个窗口
- 完整代码(window窗口+三角形)
- opengl的初始化过程
- 顶点数据VAO
- 缓冲对象
- 目标对象target(指定当前context需要激活的缓存对象)
- 标准化设备坐标系统NDC
- 顶点和片元着色器
- VAP顶点属性指针
- 第一次使用opengl进行渲染
- 清楚帧缓存
- 绘制指令:实现顶点数据向 OpenGL 管线的传输
- 启用和禁用opengl的操作
1.1 什么是opengl
-
是一种应用程序编程接口API,是一种可以对图形硬件设备特性进行访问的软件库。
-
是跨平台的图形渲染API,不需要考虑计算机操作系统或窗口系统,可在多种不同的图形硬件系统上,或者完全通过软件的方式(如果当前系统没有图形硬件)实现Opengl的接口。
-
opengl自身是不具有执行窗口或处理用户输入 / 交互的函数,也不具有任何表达三维物体模型或读取图像或模型文件的操作。前者我们一般借助应用程序所运行的窗口系统提供的窗口来替代,后者用一系列几何图元(geometric primitive,包括点、线、三角形、面片)来创建三维空间的物体。
-
OpenGL是使用客户端一服务端的形式实现的,我们编写的应用程序可以看做客户端,而计算机图形硬件厂商所提供的OpenGL实现可以看做服务端。OpenGL的某些实现(例如X窗口系统的实现)允许服务端和客户端在一个网络内的不同计算机上运行。这种情况下客户端负责提交OpenGL命令,这些OpenGL命令然后被转换为窗口系统相关的协议,通过共享网络传输到服务端,服务端最终执行并产生图像内容。
openg程序基本所需执行的操作
- 从opengl的几何图元中设置数据,用于构建形状。
- 使用不同的着色器(shader)对输入的图元数据执行计算操作,判断它们的位置、颜色,以及其他渲染属性。
- 将输入的图元的数学描述转换为屏幕位置对应的像素片元。这一步也叫光栅化rasterization,如果opengl中的片元若最终渲染为图像,那么该片元就是像素。
- 最后,针对光栅化过程中的每个片元,执行片元着色器fragment shader,从而决定这个片元的最终颜色和位置。
- 如果有必要,还需要对每个片元执行一些额外的操作,例如判断片元对应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合,即混合与测试。
1.2 初识opengl程序
因为可以用opengl去做那么多事情,所以opengl程序可能会写的非常庞大和复杂。不过所有opengl程序的基本结构通常都是类似的:
- 初始化物体渲染所对应的状态。
- 设置需要渲染的物体。
基础图形学名词
- 渲染render,表示计算机从模型创建到最终图像的过程。
- opengl只是一种基于光栅化的渲染系统,当然也有例如使用光线追踪来生成图像。
- OpenGL的最新版本如今已经变得更加灵活而强大因此诸如光线跟踪、光子映射(photon mapping)、路径跟踪(path tracing)以及基于图像的渲染(image-based rendering)这样的技术都可以相对简单地在可编程图形硬件端实现了。
-
模型model / 场景对象,是通过几何图元,例如点、线和三角形来构建的,而图元与模型的顶点(vertex)也存在着各种对应的关系(根据图元类型不同,如果规划顶点来构成对象不同)。
-
着色器shader,是一种图形硬件设备所执行的一类特殊函数,可以看作为专为图形处理单元GPU编译的一种小型程序。
- opengl在其内部包含所有的编译器工具,可以直接从着色器源代码创建GPU所需的编译代码并执行。
- 在opengl中会用到6种不同的着色阶段shader stage,其中最常用的包括顶点着色器vertex shader(处理顶点数据)以及片元着色器fragment shader(处理光栅化后的片元数据)。
- 像素pixel,最后生成的图像包含了屏幕上绘制的所有像素点,像素是显示器上最小的可见单元。
- 帧缓存framebuffer,计算机系统将计算的所有的像素保存到帧缓存中,帧缓存是由图形硬件设备管理的一块独立内存区域,可以直接映射到最终的显示设备上。
第三方库
- GLAD/ GLEW都可以自动识别平台所支持的opengl高级扩展函数,可以获取不同平台下的opengl在显卡中具体实现的函数指针。如果没有这些类型的库,windows下需要通过函数指针调用显卡的函数,但是显卡驱动具体函数的地址,运行时才知道。示例:
// define the function's prototype原型
typedef void(*GL_GENBUFFERS)(GLsizei, Gluint*);// GL_GENBUFFERS为函数指针类型名
// find the function and assign it to a function pointer
// wgl为windows窗口系统扩展
// wglGetProcAddress可以获取显卡在当前上下文状态下的opengl函数的地址
GL_GENBUFFERS glGenBuffers =(GL_GENBUFFERS)wglGetProcAddress("glGenBUffers");
// function can now be called as normal
unsigned int buffer;
glGenBuffers(1, &buffer);
- glfw,跨平台工具库,提供了一些渲染物体所需的最低限度接口,例如定义上下文,管理窗口,读取输入,处理事件。
GLFW可以使得OpenGL程序的创建过程变得简单,因为它的基础形式只需要四个步骤就可以完成应用程序的创建:
- 初始化 GLFW库。
- 创建一个 GLFW窗口以及 OpenGL 环境。
- 渲染你的场景。
- 渲染你的场景。
http://www.glfw.org
1.3 opengl语法
- opengl库中所有函数都会以字符gl作为前缀。
- opengl库中所有常量都是以GL_作为前缀。都是通过#defines来完成,基本上都可以在opengl的头文件glcorearb.h和glext.h中找到。
- 为了能够方便地在不同的操作系统之间移植OpenGL程序,OpenGL还为函数定义了不同的数据类型,例如GLfloat是浮点数类型。
使用c语言的数据类型直接表示opengl数据类型时,因为opengl自身的实现不同,可能会造成类型不匹配,不能跨平台了。
- 由于OpenGL是一个C语言形式的库,因此它不能使用函数的重载来处理不同类型的数据,此时它使用函数名称的细微变化来管理实现同一类功能的函数集。
例如glUniform*()的函数,它有多种变化形式,例如 glUniform2f()和glUniform3fv()。glUniform3fv表示需要用一个一维的GLfloat数组来传入3个浮点数值,v代表vector。
数据类型
1.4 渲染管线rendering pipeline
opengl实现了我们通常所说的渲染管线,即一系列数据处理过程,并将应用程序的数据转换到最终渲染的图形。
以下是opengl 4.5版本:
opengl3.0版本:
- 光栅化单元负责对所有剪切区域clipping region内的图元生成片元数据,然后对每一个片元都执行一个片元着色器。
- 可以完全控制自己需要用到的着色器来实现自己所需的功能。我们不需要用到所有的着色阶段,事实上,只有顶点着色器和片元着色器是必需的。细分和几何着色器是可选的步骤。
准备向opengl传输数据(缓存初始化)
OpenGL需要将所有的数据都保存到**缓存对象(buffer object)**中,它相当于由 OpenGL维护的一块内存区域。我们可以使用多种方式来创建这样的数据的缓存,不过最常用的方法就是使用例1.1中的 gINamedBufferStorage()命令同时设置缓存的大小及内容。我们可能还需要对缓存做一些额外的设置,相关的内容请参见后续。
将数据传输到opengl
- 当将缓存初始化完毕之后,我们可以通过调用OpenGL的一个绘制命令来请求渲染几何图元,例如 gIDrawArrays()。
- OpenGL的绘制通常就是将顶点数据(OpenGL维护的一块内存区域)传输到openGL服务端(计算机图形硬件厂商所提供的OpenGL实现的地方),我们可以将一个顶点视为一个需要统一处理的数据包。这个包中的数据可以是我们需要的任何数据(也就是说,我们自己负责定义构成顶点的所有数据),通常其中几乎始终/ 至少 会包含位置数据。其他的数据可能用来决定一个像素的最终颜色(用于各阶段着色器进行计算等功能)。
- 而使用buffer的好处就是可以一次性发送一大批数据到显存中,而且着色器几乎能立即访问顶点数据。
顶点着色
- 对于绘制命令传输的每个顶点,OpenGL都会调用一个顶点着色器来处理顶点相关的数据。
- 通常来说,一个复杂的应用程序可能包含许多个顶点着色器,但是在同一时刻只能有一个顶点着色器起作用。
细分着色(几何图元数量增加,模型外观变平顺)
- 顶点着色器处理每个顶点的关联数据之后,如果同时激活了细分着色器(tessellation shader),那么它将进一步处理这些数据。
- 细分着色器会使用面片(patch)来描述一个物体的形状,并且使用相对简单的面片几何体连接来完成细分的工作,其结果是几何图元的数量增加,并且模型的外观会变得更为平顺。
- 细分着色阶段会用到两个着色器(控制和计算着色器)来分别管理面片数据并生成最终的形状。
几何着色
- 允许在光栅化之前对每个几何图元做更进一步的处理,例如创建新的图元。
图元装配
- 经过前面的顶点数据的处理,图元装配阶段将这些顶点和相关的几何图元匹配组织起来,准备下一步的剪切和光栅化工作。
剪切
- 顶点可能会落在视口viewport之外(就是我们可以进行绘制的窗口区域),此时与顶点相关的图元会做出改动,以保证相关的像素不会在视口外绘制,这一过程叫做剪切clipping,它是由opengl自动完成。
光栅化
- 剪切之后马上要执行的工作,就是将更新后的图元传递到光栅化(rasterizer)单元,生成对应的片元。
- 光栅化的工作是判断某一部分几何体(点、线或者三角形)所覆盖的屏幕空间。
- 得到了屏幕空间信息以及输入的顶点数据之后,光栅化单元就可以直接对片元着色器中的每个可变变量进行线性插值,然后将结果值传递给用户的片元着色器。
OpenGL 实现光栅化和数据插值的方法是与具体平台相关的。我们无法保证在不同平台上的插值结果总是相同的。
- 我们可以将一个片元视为一个“候选的像素”,也就是可以放置在帧缓存中的像素,但是它也可能被最终剔除,不再更新对应的像素位置。
- 光栅化意味着一个片元的生命开始,而片元着色器中的计算过程本质上意味着计算这个片元的最终颜色。
片元着色
- 最后一个可以通过编程控制屏幕上显示颜色的阶段叫做片元着色阶段。
- 显示颜色的阶段叫做片元着色阶段。在这个阶段中我们使用着色器来计算片元的最终颜色(尽管在下一个阶段(逐片元的操作)时可能还会最终改变一次颜色)和它的深度值。
- 在这里我们还可能会会使用纹理映射的方式,对顶点处理阶段所计算的颜色值进行补充(纹理贴图)。
- 如果我们觉得不应该继续绘制某个片元在片元着色器中还可以终止这个片元的处理,这一步叫做片元的丢弃(discard)。
顶点着色(包括细分和几何着色)决定了一个图元应该位于屏幕的什么位置,而片元着色使用这些信息来决定某个片元的颜色应该是什么。
逐片元的操作(混合与测试)
- 最后的独立片元处理过程。在这个阶段里会使用深度测试(depth test,或者通常也称作z缓存)和模板测试(stencil test)的方式来决定一个片元是否是可见的。
- 如果一个片元成功地通过了所有激活的测试,那么它就可以被直接绘制到帧缓存中了,它对应的像素的颜色值(也可能包括深度值)会被更新,如果开启了融混(blending)模式那么片元的颜色会与该像素当前的颜色相叠加,形成一个新的颜色值并写入帧缓存中(混合算法可以设置)。
1.5 第一个程序(绘制三角形) / 配置环境
main函数——创建一个窗口
int main()
{
// 使用glfw设置和打开一个渲染用的窗口。详细介绍参见 附录 A 第三方支持库
// Initialize GLFW
glfwInit();
// Tell GLFW what version of OpenGL we are using
// In this case we are using OpenGL 4.5
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
// Tell GLFW we are using the CORE profile
// So that means we only have the modern functions
// 使用核心模式必需使用VAO顶点数组!
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Create a GLFWwindow object of 800 by 800 pixels, naming it "dankokokoOpenGL"
// 还创建了一个与窗口关联的opengl设备环境,在使用该环境前,我们需要设置它为当前环境
GLFWwindow* window = glfwCreateWindow(800, 800, "dankokokoOpenGL", NULL, NULL);//窗口宽高
// Error check if the window fails to create
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
// Introduce the window into the current context
glfwMakeContextCurrent(window);
// Main while loop
// 负责一直处理窗口和操作系统的用户输入等操作
while (!glfwWindowShouldClose(window))
{
// Specify the color of the background
glClearColor(0.07f, 0.13f, 0.17f, 1.0f);
// Clean the back buffer and assign the new color to it
glClear(GL_COLOR_BUFFER_BIT);
// Swap the back buffer with the front buffer
glfwSwapBuffers(window);// 展现绘制内容
// Take care of all GLFW events
glfwPollEvents();// 检查操作系统返回的任何信息
}
// Delete window before ending the program
glfwDestroyWindow(window);// 清理窗口
// Terminate GLFW before ending the program
glfwTerminate();// 关闭glfw库
return 0;
}
完整代码(window窗口+三角形)
#include<iostream>
#include<glad/glad.h>
#include<GLFW/glfw3.h>
#include <GL/gl.h>
// Vertex Shader source code
const char* vertexShaderSource = "#version 450 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";
//Fragment Shader source code
const char* fragmentShaderSource = "#version 450 core\n"
"out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(0.8f, 0.3f, 0.02f, 1.0f);\n"
"}\n\0";
int main()
{
// 使用glfw设置和打开一个渲染用的窗口。详细介绍参见 附录 A 第三方支持库
// Initialize GLFW
glfwInit();
// Tell GLFW what version of OpenGL we are using
// In this case we are using OpenGL 4.5
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 5);
// Tell GLFW we are using the CORE profile
// So that means we only have the modern functions
// 使用核心模式必需使用VAO顶点数组!
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Create a GLFWwindow object of 800 by 800 pixels, naming it "dankokokoOpenGL"
// 还创建了一个与窗口关联的opengl设备环境,在使用该环境前,我们需要设置它为当前环境
GLFWwindow* window = glfwCreateWindow(800, 800, "dankokokoOpenGL", NULL, NULL);//窗口宽高
// Error check if the window fails to create
if (window == NULL)
{
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
// Introduce the window into the current context
glfwMakeContextCurrent(window);
// Load GLAD so it configures OpenGL
// 简化获取函数地址,包含了可以跨平台使用的其他一些opengl编程方法。
gladLoadGL();// 类似别的库还有glewInit,gl3wInit.
const GLubyte* glVersion = glGetString(GL_VERSION);
std::cout << "OpenGL Version: " << glVersion << std::endl;
// Specify the viewport of OpenGL in the Window
// In this case the viewport goes from x = 0, y = 0, to x = 800, y = 800
glViewport(0, 0, 800, 800);// 窗口里面opengl的展现区域,变形将会导致渲染结果变形
// Create Vertex Shader Object and get its reference
GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
// Attach Vertex Shader source to the Vertex Shader Object
// 参数2:GLchar *const*string
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
// Compile the Vertex Shader into machine code
glCompileShader(vertexShader);
// Create Fragment Shader Object and get its reference
GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
// Attach Fragment Shader source to the Fragment Shader Object
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
// Compile the Vertex Shader into machine code
glCompileShader(fragmentShader);
// Create Shader Program Object and get its reference
GLuint shaderProgram = glCreateProgram();
// Attach the Vertex and Fragment Shaders to the Shader Program
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
// Wrap-up/Link all the shaders together into the Shader Program
glLinkProgram(shaderProgram);
// Delete the now useless Vertex and Fragment Shader objects
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// Vertices coordinates
// 代表了3个顶点的空间位置
const GLfloat vertices[] =
{
-0.5f, -0.5f * float(sqrt(3)) / 3, 0.0f, // Lower left corner
0.5f, -0.5f * float(sqrt(3)) / 3, 0.0f, // Lower right corner
0.0f, 0.5f * float(sqrt(3)) * 2 / 3, 0.0f // Upper corner
};
// Create reference containers for the Vertex Array Object and the Vertex Buffer Object
GLuint VAO, VBO;
// Generate the VAO and VBO with only 1 object each
// opengl会分配一部分顶点数组对象的名称供我们使用
glGenVertexArrays(1, &VAO);
glCreateBuffers(1,&VBO);// 或glGenBuffers(1, &VBO);
// Make the VAO the current Vertex Array Object by binding it
glBindVertexArray(VAO);
// Bind the VBO specifying it's a GL_ARRAY_BUFFER
///glBindBuffer(GL_ARRAY_BUFFER, VBO);
// Introduce the vertices into the VBO
//glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
//glNamedBufferStorage(VBO, sizeof(vertices), vertices, /*GL_DYNAMIC_STORAGE_BIT*/0);
glNamedBufferData(VBO, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// Configure the Vertex Attribute so that OpenGL knows how to read the VBO
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, /*3 * sizeof(float)*/0, (void*)0);
// Enable the Vertex Attribute so that OpenGL knows to use it
glEnableVertexAttribArray(0);
// Bind both the VBO and VAO to 0 so that we don't accidentally modify the VAO and VBO we created
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// Main while loop
// 负责一直处理窗口和操作系统的用户输入等操作
while (!glfwWindowShouldClose(window))
{
// Specify the color of the background
//glClearColor(0.07f, 0.13f, 0.17f, 1.0f);
static const float black[] = { 0.0f,0.0f,0.0f,0.0f };
glClearBufferfv(GL_COLOR, 0, black);
// Clean the back buffer and assign the new color to it
glClear(GL_COLOR_BUFFER_BIT);
// Tell OpenGL which Shader Program we want to use
glUseProgram(shaderProgram);
// Bind the VAO so OpenGL knows to use it
glBindVertexArray(VAO);
// Draw the triangle using the GL_TRIANGLES primitive
glDrawArrays(GL_TRIANGLES, 0, 3);
// Swap the back buffer with the front buffer
glfwSwapBuffers(window);// 展现绘制内容
// Take care of all GLFW events
glfwPollEvents();// 检查操作系统返回的任何信息
}
// Delete all the objects we've created
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
// Delete window before ending the program
glfwDestroyWindow(window);// 清理窗口
// Terminate GLFW before ending the program
glfwTerminate();// 关闭glfw库
return 0;
}
opengl的初始化过程
顶点数据VAO
一个顶点数组实际上可以通过指定顶点属性指针,合并顶点缓冲区和索引缓冲区。绘制时可以用VAO进行指定渲染。
//如果n是负数,产生GL_INVALID_VALUE错误。
// 返回n个未使用的对象名数组arrays中,返回的名字可以用来配置更多缓存对象。
// 这里的名称类似c语言中的一个指针变量。
void glCreateVertexArrays(GLsizei n, GLuint *arrays);
// 绑定到opengl环境中以便使用。
// 果输人的变量array非0,并且是 gICreateVertexArrays()所返回的,
// 那么会激活这个顶点数组对象,并且直接影响对象中所保存的顶点数组状态。
// 如果输人的变量 array为0,那么OpenGL将不再使用之前绑定的顶点数组。
// 绑定很像铁路的道岔开关,绑定哪个,从这条路线通过的所有列车都会驶向对应的轨道。
// 一般create和初始化一个对象之后,或者每当我们准备使用这个对象,
// 但是当前绑定并不是该对象时,我们都应该绑定。
// 如果 array不是glCreateVertexArrays()所返回的数值,
// 或者它已经被glDeleteVertexArrays()函数释放了,那么这里将产生一个GL_INVALID_OPERATION 错误。
void glBindVertexArray(GLuint array);
// Delete all the objects we've created
// 删除已经定义的VAO,这样VAO名称将可以再次用来create新的顶点数组。
// 且当前绑定的VAO被重设为0.
void glDeleteVertexArrays(GLsizei n, const GLuint *arrays);
// 可以使用glIsVertexArray来检查一个VAO名称是否已经被被使用,即被保留为一个顶点数组对象了,
// 是则返回GL_TRUE
GLboolean glIsVertexArray(GLuint array);
缓冲对象
-
缓冲对象是opengl服务端(计算机图形硬件厂商所提供的OpenGL实现)分配和管理的一块内存区域,并且几乎所有传入openGL的数据都是存储在缓存对象当中的。
-
分配缓存对象
// 返回n个当前未使用的缓存对象名称,并保存到bufers数组中,未初始化,使用需要绑定。
// 返回到buffers中的名称不一定是连续的整型数据。
// 如果n是负数,那么产生GL_INVALID_VALUE 错误。
// 0是一个保留的缓存对象名称,gICreateBufers()永远都不会返回这个值的缓存对象。
void glGenBuffers(GLsizei n, GLuint *buffers);
// opengl4.5 直接创建并返回一个已经初始化的缓冲对象标识符,无需立即绑定也可以直接对它进行配置。
// 搭配glNamedBufferData或glNamedBufferStorage(最后一个参数置0)使用.
void glCreateBuffers(GLsizei n, GLuint *buffers);
// 绑定缓冲区到当前opengl环境中(当前上下文current context)
// 绑定缓存的类型也称作绑定目标(binding target)。
// 顶点数据保存在缓存中,需要绑定到GL_ARRAY_BUFFER的目标对象
// buffer参数则是要绑定的缓存对象名称
// 如果绑定的bufer值为0,那么OpenGL将不再对当前 target 使用任何缓存对象。
void glBindBuffer(GLenum target, GLuint buffer);
// 所有的缓存对象都可以使用 gIDeleteBuffers()直接释放。
// 如果删除的缓存对象已经被绑定,那么该对象的所有绑定将会重置为默认的缓存对象,即相当于用0作为参数执行glBindBufer()的结果。
// 如果试图删除不存在的缓存对象,或者缓存对象为0,那么将忽略该操作(不会产生错误)。
void glDeleteBuffers(GLsizei n, const GLuint *buffers);
// 判断一个整数值是否是一个缓存对象的名称。
GLboolean gllsBuffer(GLuint buffer);
- 将数据载入缓存对象
// 分配顶点数据所需的存储空间,然后将数据从应用程序的数组中拷贝到OpenGL管理的一片内存中,不需要被绑定
// 因为不需要绑定,那么在绑定VAO后,使用VAP(即调用glVertexAttribPointer)前需要绑定缓存!
// 在OpenGL服务端内存中分配size个存储单元(通常为byte)
// 如果data传人NULL那么将保留 size 大小的未初始化的数据,以备后用。
// flags 提供了缓存中存储的数据相关的用途信息。它是一系列标识量经过逻辑“与运算的总和。
// 需的 size 大小超过了服务端能够分配的额度,那么gINamedBufferData()将产生一个GL OUT OF MEMORY错误。
// 如果flags包含的不是可用的模式值,那么将产生GLINVALID VALUE 错误。
void glNamedBufferStorage(GLuint buffer, GLsizeiptr size, const void *data, GLbitfield flags).
void glNamedBufferData(GLuint buffer, GLsizeiptr size, const void *data, GLenum usage);
// 是对绑定到目标对象target的缓存进行操作.
void glBufferData(GLenum target, GLsizeiptr size, const void *data, GLenum usage);
void glBufferStorage(GLenum target, GLsizeiptr size, const void *data, GLbitfield flags);
flags:
GL_DYNAMIC_STORAGE_BIT、GL_MAP_READ_BIT、GL_MAP_WRITE_BIT、GL_MAP_PERSISTENT_BIT、GL_MAP_COHERENT_BIT 和 GL_CLIENT_STORAGEBIT。
含义参考
- 注意需要opengl 4.5,需要配置glad对应版本:参考
- 使用
const GLubyte* glVersion = glGetString(GL_VERSION);
可以检查本机支持的opengl版本。
目标对象target(指定当前context需要激活的缓存对象)
target必须设置为以下类型中的一个:
GL_ARRAY_BUFFER、GL_ATOMIC_COUNTER_BUFFER、GL_ELEMENT_ARRAY_BUFFER、
GL_PIXEL_PACK_BUFFER、GL_PIXEL_UNPACK_BUFFER、GL_COPY_READ_BUFFER、
GL_COPY_WRITE_BUFFER、GL_SHADER_STORAGE_BUFFER、GL_QUERY_RESULT_BUFFER、
GL_DRAW_INDIRECT_BUFFER、GL_TRANSFORM_FEEDBACK_BUFFER 和GL_UNIFORM_BUFFER。
标准化设备坐标系统NDC
- OpenGL只能够绘制坐标空间内的几何体图元,而具有该范围限制的坐标系统也称为规格化设备坐标系统(NormalizedDeviceCoordinate,NDC)。
- 该坐标系其实是在透视除法之后,并且把在投影矩阵的frustum压缩成cube,并且把xyz都限制在[-1,1]之间构成的。
- 后续章节会介绍将三维空间中的复杂物体陕射到规格化设备坐标系中的数学方法。
顶点和片元着色器
- 每一个OpenGL程序进行绘制的时候,都需要指定至少两个着色器:顶点着色器和片元着色器(不指定则会使用默认的)。
- 最基础的vs和fs:
// Vertex Shader source code
const char* vertexShaderSource = "#version 450 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";
//Fragment Shader source code
const char* fragmentShaderSource = "#version 450 core\n"
"layout (location = 0) out vec4 FragColor;\n"
"void main()\n"
"{\n"
" FragColor = vec4(0.8f, 0.3f, 0.02f, 1.0f);\n"
"}\n\0";
- 如果第一行没有指定glsl版本和模式,则会默认使用110版本,但是这与core模式时不兼容的。
layout (location = 0) in vec3 aPos;
中如果我们使用vec4,但是我们传入的是vec3,则OpenGL会用默认数值自动填充这些缺失的坐标值,vec4的默认值为(0.0,0.0,0.0,1.0)。layout (location = 0)
做布局限定符(layout qualifier),目的是为变量提供元数据(metadata),可以设置很多不同的属性,其中有些是与不同的着色阶段相关的。location = 0
的设置则与调用glVertexAttribPointer和glEnableVertexAttribArray共同起作用有关。layout (location = 0) out
,着色器可以设置多个输出值,而某个变量所对应的输出结果就是通过location来设置的。
VAP顶点属性指针
glVertexAttribPointer和glEnableVertexAttribArray
两个函数指定了顶点着色器的变量与我们存储在缓存对象中数据的关系。这也就是我们所说的着色管线装配的过程,即将应用程序与着色器之间,以及不同着色阶段之间的数据通道连接起来。- 需要在着色器中声明一个in 变量,然后使用 gIVertexAttribPointer()将它关联到一个顶点属性数组VAO,即调用该函数则会自动将当前绑定 / 激活的VBO和该VAP绑定到当前绑定 / 激活的VAO中,EBO同VBO一样。
// index(着色器中的属性位置)位置对应的数据值,即location
// pointer表示缓存对象中从起始位置开始计算的,到当前属性数据的偏移值(假设起始地址为0),使用基本的系统单位(byte)。
// 例如数据是由多个{位置,颜色,纹理坐标}的元素构成,则位置偏移为(void*)0,颜色偏移则是位置的大小(单位byte)。
// size表示每个顶点需要更新的分量数目,可以是1、2、3、4或者GL BGRA;例如{位置,颜色,纹理坐标}一般是3,4,2总共为9分量。
// type 指定了数组中每个元素的数据类型。
// normalized 设置顶点数据在存储前是否需要进行归一化(或者使用 glVertexAttribFourN*()函数)。
// stride是数组中每两个元素之间的大小偏移值(byte)。stride是数组中每两个元素之间的大小偏移值(byte)。
// Configure the Vertex Attribute so that OpenGL knows how to read the VBO
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized,GLsizei stride, const GLvoid *pointer);
type:
GL_BYTE、GL_UNSIGNED_BYTE、GL_SHORT、GL_UNSIGNED_SHORT、GL_INT、GL_UNSIGNED_INT、GL_FIXED、GL_HALF_FLOAT、GL_FLOAT或GL_DOUBLE
// 启用顶点属性数组。设置是否启用与index索引相关联的顶点数组.
// Enable the Vertex Attribute so that OpenGL knows to use it
void glEnableVertexAttribArray(GLuint index);
void glDisableVertexAttribArray(GLuint index);
- 我们刚刚使用gIVertexAttribPointer()和 glEnableVertexAttribArray()设置的状态是在上文就绑定好的顶点数组目标对象中的,即需要绑定VBO和VAO当当前上下文。
- 如果希望设置一个顶点数组对象,但是不要把它绑定到设备环境中,那么可以调用glEnableVertexArrayAttrib()、gIVertexArrayAttribFormat()和 gIVertexArrayVertexBuffers().也就是通过直接状态访问(directstate access)的模式来完成相同的操作。
- 着色器初始化,编译链接,使用等接口见后文
第一次使用opengl进行渲染
- 实际上前面每次初始化好后,都应该使用
glBindBuffer(GL_ARRAY_BUFFER, 0);和glBindVertexArray(0);
等操作取消绑定,我们只需要记录缓存名称,等调用绘制指令 / 使用 前再次绑定VAO(因为通过VAP已经将VBO和EBO绑定到VAO了)即可。 - 下列在while循环中的代码在所有opengl程序中都会用到:
// Main while loop
// 负责一直处理窗口和操作系统的用户输入等操作
while (!glfwWindowShouldClose(window))
{
// Specify the color of the background
glClearColor(0.07f, 0.13f, 0.17f, 1.0f);
// Clean the back buffer and assign the new color to it
glClear(GL_COLOR_BUFFER_BIT);
// 上面两句可用下面替代:
/*
static const float black[] = { 0.0f,0.0f,0.0f,0.0f };
glClearBufferfv(GL_COLOR,0,black);
*/
// Tell OpenGL which Shader Program we want to use
glUseProgram(shaderProgram);// 可选,因为可以使用默认的着色器。
// Bind the VAO so OpenGL knows to use it
glBindVertexArray(VAO);
// Draw the triangle using the GL_TRIANGLES primitive
glDrawArrays(GL_TRIANGLES, 0, 3);
// Swap the back buffer with the front buffer
glfwSwapBuffers(window);// 展现绘制内容
// Take care of all GLFW events
glfwPollEvents();// 检查操作系统返回的任何信息
}
清楚帧缓存
// 首先要清除帧缓存的数据再进行渲染。
// 清除当前绘制帧缓存中的指定缓存类型,清除结果为value(用value覆盖,或者说清楚后,赋予结果为value)。
// 参数buffer设置了要清除的缓存类型,它可以是GL_COLOR、GL_DEPTH,或者GL_STENCIL。
// 因为一个帧缓存中一般包含颜色附件,深度附件和模板附件。
// 参数drawbuffer设置了要清除的缓存索引。
//如果当前绑定的是默认帧缓存,或者buffer 设置为GL_DEPTH或GL_STENCIL,那么 drawbuffer 必须是0。
// 因为默认帧缓存只有一个深度和模板附件。
// 否则它表示需要被清除的颜色缓存的索引。
// 参数 value是一个数组的指针,其中包含了一个或者四个浮点数,取决于需要清楚的帧缓存中的缓存类型。
void glClearBufferfv(GLenum buffer, GLint drawbuffer, const GLfloat *value);
value参数:
- 如果 bufer设置为GLCOLOR,那么 value 必须是一个最少四个数值的数组,以表示颜色值。
- 如果bufFer是GLDEPTH或者GL STENCI,那么value 可以是一个单独的浮点数,分别用来设置深度缓存或者模板缓存清除后的结果。
- 清楚帧缓存的方法下面的搭配:
- 首先调用glClearColor()、glClearDepth()及glClearStencil()分别设置清空后颜色缓存、深度缓存和模板缓存中的默认值。
- 然后调用glClear()并传入GL_COLOR_BUFFER_BIT、GL_DEPTH_BUFFER_BIT、GL_STENCIL_BUFFER_BIT或它们的位组合进行对应缓存的清空操作。
绘制指令:实现顶点数据向 OpenGL 管线的传输
OpenGL的绘制通常就是将顶点数据(OpenGL维护的一块内存区域,即当前上下文中绑定激活的缓存)传输到openGL服务端(计算机图形硬件厂商所提供的OpenGL实现的地方),我们可以将一个顶点视为一个需要统一处理的数据包。
// 绑定的VAO中VBO的起始位置为first,而结束位置为 first+count-1。
// mode设置了构建图元的类型。
void glDrawArrays(GLenum mode, GLint first, GLsizei count);
// 绑定的VAO绑定了EBO则参数indices可以为空指针
void glDrawElements(GLenum mode, GLsizei count, GLenum type, const void *indices);
// glDrawArrays的复杂版本
void glDrawArraysInstancedBaseInstance(GLenum mode, GLint first, GLsizei count, GLsizei instancecount, GLuint baseinstance);
mode几何图元类型:
GL_POINTS、GL_LINES、GL_LINE_STRIP、GL_LINE_LOOP、GL_TRIANGLES、
GL_TRIANGLESTRIP、GL_TRIANGLE_FAN和GL_PATCHES(需要启动细分着色器,参考) 中的任意一种。
启用和禁用opengl的操作
- 在第一个例子当中有一个重要的特性并没有用到,但是在后文中我们会反复用到它那就是对于 OpenGL操作模式的启用和禁用。
- 绝大多数的操作模式都可以通过 glEnable()和 glDisable()命令开启或者关闭。
void glEnable(GLenum capability);
void glDisable(GLenum capability);
// 判断是否启用了
GLboolean gllsEnabled(GLenum capability);
例如GL_DEPTH_TEST可以用来开启或者关闭深度测试;GL_BLEND可以用来控制融合的操作,而GL_RASTERIZER_DISCARD用于transform feedback过程中的高级渲染控制。