OpenGL入门——第一个三角形
一、渲染管线
在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,所以由OpenGL的图形渲染管线将3D坐标转为2D坐标。
图形渲染管线主要分为2个部分: 第一部分将3D坐标转为2D坐标,第二部分把2D坐标转为实际的颜色像素。
注意:2D坐标和像素也是不同的,2D坐标精确表示一个点在2D空间中的位置(可以为小数),而2D像素是这个点的近似值,2D像素受到屏幕/窗口分辨率的限制(只能是整数)。
3D坐标-->【图形渲染管线】-->2D像素
图形渲染管线分为几个阶段,每个阶段的输入是前一个阶段的输出。
这些阶段都是高度专门化的(它们都有一个特定函数),并且很容易执行,而且它们具有并行执行的特性。
当今大多数显卡都有成千上万的小处理核心,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理数据。这些小程序叫做着色器(Shader)。
有些着色器可以自己配置,因为允许用自己写的着色器来代替默认的,所以能够更细致地控制图形渲染管线中的特定部分了
OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写的。
图形渲染管线划分的阶段:顶点数据->顶点着色器(无默认,须自定义)->形状(图元)装配->几何着色器(一般默认)->光栅化->片段着色器(无默认,须自定义)->测试与混合
1. 顶点着色器:把一个单独的顶点作为输入。顶点着色器主要的目的是把3D坐标转为另一种3D坐标,同时顶点着色器允许我们对顶点属性进行一些基本处理。一旦你的顶点坐标已经在顶点着色器中处理过,它们就应该是标准化设备坐标了,标准化设备坐标是一个x、y和z值在-1.0到1.0的一小段空间。任何落在范围外的坐标都会被丢弃/裁剪,不会显示在你的屏幕上。
2. 图元装配:将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并所有的点装配成指定图元的形状。
3. 几何着色器(可选,通常使用默认的):图元装配阶段的输出会传递给几何着色器(Geometry Shader)。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。
4. 光栅化阶段:把图元映射为最终屏幕上相应的像素,生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。
5. 片段着色器:计算一个像素的最终颜色,通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色
6. 测试和混合阶段:在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段,我们叫做Alpha测试和混合(Blending)阶段。这个阶段检测片段的对应的深度(和模板(Stencil))值(后面会讲),用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。所以,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
二、渲染对象
1. 顶点数组对象VAO(Vertex Array Objec)
2. 顶点缓冲对象VBO(Vertex Buffer Object)
3. 元素缓冲对象EBO(Element Buffer Object),或索引缓冲对象IBO(Index Buffer Object)
三、绘制对象
1. 窗口初始化、输入、回调函数:
1 //GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h),所以需要在其它依赖于OpenGL的头文件之前包含GLAD 2 #include <glad/glad.h> 3 #include <GLFW/glfw3.h> 4 #include <iostream> 5 6 //改变窗口大小 7 void framebuffer_size_callback(GLFWwindow* window, int width, int height) 8 { 9 glViewport(0, 0, width, height); 10 } 11 12 //输入 13 void processInput(GLFWwindow *window) 14 { 15 if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)//点击ESC键退出绘制 16 glfwSetWindowShouldClose(window, true); 17 } 18 19 GLFWwindow* init_window() 20 { 21 ///窗口初始化 22 glfwInit(); 23 glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);//主版本号,当API以不兼容的方式更改时,该值会增加。 24 glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);//次版本号,当特性被添加到API中时,它会增加,但是它保持向后兼容。 25 glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);//使用核心模式,不兼容已废弃函数 26 27 //创建glfw窗口 28 GLFWwindow* window = glfwCreateWindow(800, 600, "ping-window", NULL, NULL); 29 if (window == NULL) 30 { 31 std::cout << "failed to create GLFW window" << std::endl; 32 glfwTerminate();//释放/删除之前的分配的所有资源 33 return nullptr; 34 } 35 glfwMakeContextCurrent(window);//将窗口的上下文设置为当前线程的主上下文 36 glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);//注册为调整窗口回调函数 37 38 //GLAD是用来管理OpenGL的函数指针的,在调用任何OpenGL的函数之前初始化GLAD 39 if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))//给GLAD传入了用来加载系统相关的OpenGL函数指针地址的函数 40 { 41 std::cout << "failed to intialize GLAD" << std::endl; 42 return nullptr; 43 } 44 45 glViewport(0, 0, 800, 600);//处理过的OpenGL坐标范围只为-1到1,因此我们事实上将(-1到1)范围内的坐标映射到(0, 800)和(0, 600) 46 47 return window; 48 }
2. 着色器程序
用着色器语言GLSL(OpenGL Shading Language)编写顶点着色器和片段着色器,然后编译这个着色器,这样就可以在程序中使用它了。
2.1顶点着色器:
1 //vertex shader source 2 #version 330 core 3 layout(location = 0) in vec3 position; //位置变量的属性位置为0 4 5 void main() 6 { 7 gl_Position = vec4(position, 1.0); //opengl顶点坐标 8 }
1)#version 330 core:使用opengl3.3及以上的核心模式
2)in关键字声明为输入
3)layout(location = 0)指定输入变量的位置值,如果输入不只是位置坐标,那么还有layout(location = 1).....
4)gl_Position顶点的坐标
5)vec3、vec4、vecn表示n维向量
2.2片段着色器:
1 //fragment shader source 2 #version 330 core 3 out vec4 fragColor; //像素的最终颜色 4 5 void main() 6 { 7 fragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);//最终的输出颜色 8 }
1)out关键字声明为输出
2)fragColor像素的最终颜色
在程序中将着色器程序以字符串的方式附加到着色器对象上,然后编译它。定义一个着色器程序对象(shaderProgram)来链接(glLinkProgram)编译完成的着色器。
const char *vertexShaderSource = "#version 330 core\n" "layout (location = 0) in vec3 aPos;// 位置变量的属性位置值为 0 \n" "void main()\n" "{\n" " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" "}\0"; 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" "}\0"; //创建一个顶点着色器对象,注意还是用ID来引用的 unsigned int vertexShader; vertexShader = glCreateShader(GL_VERTEX_SHADER); //着色器源码附加到着色器对象上 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL glCompileShader(vertexShader);//编译源码 int success; char infoLog[512]; glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 if (!success) { glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; } //创建一个片段着色器对象,注意还是用ID来引用的 unsigned int fragmentShader; fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); glCompileShader(fragmentShader);//编译源码 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 if (!success) { glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; } //创建一个着色器对程序 unsigned int shaderProgram; shaderProgram = glCreateProgram(); glAttachShader(shaderProgram, vertexShader);//把之前编译的着色器附加到程序对象上 glAttachShader(shaderProgram, fragmentShader); glLinkProgram(shaderProgram);//glLinkProgram链接它们 glGetProgramiv(shaderProgram, GL_COMPILE_STATUS, &success);//用glGetProgramiv检查是否编译成功 if (!success) { glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog); std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl; } //链接后即可删除 glDeleteShader(vertexShader); glDeleteShader(fragmentShader);
在渲染的时候激活(glUseProgram)这个着色器程序,即可绘制物体
3. 顶点输入
开始绘制图形之前,需要先给OpenGL输入一些顶点数据。OpenGL是一个3D图形库,所以在OpenGL中我们指定的所有坐标都是3D坐标(x、y和z)。OpenGL不是简单地把所有的3D坐标变换为屏幕上的2D像素;仅当3D坐标在3个轴(x、y和z)上-1.0到1.0的范围内时才处理它。所有在这个范围内的坐标叫做标准化设备坐标(Normalized Device Coordinates),此范围内的坐标最终显示在屏幕上(在这个范围以外的坐标则不会显示)。
先定义一个可画出三角形的标准化设备坐标
float vertices[] = { -0.5f, -0.5f, 0.0f, 0.5f,-0.5f, 0.0f, 0.0f, 0.5f, 0.0f };
将顶点数组输入GPU内存中,通过VBO对象管理这个内存。但是我们还需要使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据
1 //绑定到目标对象,VBO变成了一个顶点缓冲类型 2 glBindBuffer(GL_ARRAY_BUFFER, VBO);//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称 3 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//数据传入缓冲内存中,GL_STATIC_DRAW:数据不会或几乎不会改变; GL_DYNAMIC_DRAW:数据会被改变很多; GL_DYNAMIC_DRAW:数据会被改变很多 4 5 //设置顶点属性指针,如何解析顶点数据 6 /* 7 第一个参数指定我们要配置的顶点属性,顶点着色器中使用layout(location = 0)定义 8 第二个参数指定顶点属性的大小 9 第三个参数指定数据的类型 10 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间 11 第五个参数步长(Stride),它告诉我们在连续的顶点属性组之间的间隔 12 最后一个参数的类型是void*,数据在缓冲中起始位置的偏移量(Offset) 13 */ 14 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); 15 glEnableVertexAttribArray(0);//启用顶点属性layout(location = 0),顶点属性默认是禁用的 16 glBindBuffer(GL_ARRAY_BUFFER, 0);//设置完属性,解绑VBO
最后我们用一个VAO对象存储VBO及其属性,只要在绘制的时候绑定对应的VAO对象即可。
(OpenGL的核心模式要求我们使用VAO,它知道该如何处理我们的顶点输入。如果绑定VAO失败,OpenGL会拒绝绘制任何东西。)
4. 完整代码
1 const char *vertexShaderSource = "#version 330 core\n" 2 "layout (location = 0) in vec3 aPos;// 位置变量的属性位置值为 0 \n" 3 "void main()\n" 4 "{\n" 5 " gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" 6 "}\0"; 7 8 const char *fragmentShaderSource = "#version 330 core\n" 9 "out vec4 FragColor;\n" 10 "void main()\n" 11 "{\n" 12 " FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);//最终的输出颜色\n" 13 "}\0"; 14 15 int hello_triangle() 16 { 17 GLFWwindow* window = init_window(); 18 19 ///定义着色器 20 //创建一个顶点着色器对象,注意还是用ID来引用的 21 unsigned int vertexShader; 22 vertexShader = glCreateShader(GL_VERTEX_SHADER); 23 24 //着色器源码附加到着色器对象上 25 glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);//要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量,这里只有一个。第三个参数是顶点着色器真正的源码,第四个参数我们先设置为NULL 26 glCompileShader(vertexShader);//编译源码 27 int success; 28 char infoLog[512]; 29 glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 30 if (!success) 31 { 32 glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); 33 std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl; 34 } 35 36 //创建一个片段着色器对象,注意还是用ID来引用的 37 unsigned int fragmentShader; 38 fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 39 glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL); 40 glCompileShader(fragmentShader);//编译源码 41 glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);//用glGetShaderiv检查是否编译成功 42 if (!success) 43 { 44 glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); 45 std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl; 46 } 47 48 //创建一个着色器对程序 49 unsigned int shaderProgram; 50 shaderProgram = glCreateProgram(); 51 glAttachShader(shaderProgram, vertexShader);//把之前编译的着色器附加到程序对象上 52 glAttachShader(shaderProgram, fragmentShader); 53 glLinkProgram(shaderProgram);//glLinkProgram链接它们 54 glGetProgramiv(shaderProgram, GL_COMPILE_STATUS, &success);//用glGetProgramiv检查是否编译成功 55 if (!success) 56 { 57 glGetShaderInfoLog(shaderProgram, 512, NULL, infoLog); 58 std::cout << "ERROR::SHADER::PROGRAM::LINK_FAILED\n" << infoLog << std::endl; 59 } 60 61 //链接后即可删除 62 glDeleteShader(vertexShader); 63 glDeleteShader(fragmentShader);//*/ 64 65 ///定义顶点对象 66 float vertices[] = { 67 -0.5f, -0.5f, 0.0f, 68 0.5f,-0.5f, 0.0f, 69 0.0f, 0.5f, 0.0f
70 71 }; 72 73 //生成VAO对象,缓冲ID为VAO 74 unsigned int VAO; 75 glGenVertexArrays(1, &VAO); 76 glBindVertexArray(VAO);//绑定VAO,从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO,供之后使用 77 78 //生成VBO对象,缓冲ID为VBO 79 unsigned int VBO; 80 glGenBuffers(1, &VBO);//第一个参数GLsizei是要生成的缓冲对象的数量,第二个GLuint是要输入用来存储缓冲对象名称的数组 81 82 //绑定到目标对象,VBO变成了一个顶点缓冲类型 83 glBindBuffer(GL_ARRAY_BUFFER, VBO);//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称 84 glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);//数据传入缓冲内存中,GL_STATIC_DRAW:数据不会或几乎不会改变; GL_DYNAMIC_DRAW:数据会被改变很多; GL_DYNAMIC_DRAW:数据会被改变很多 85 86 //设置顶点属性指针,如何解析顶点数据 87 /* 88 第一个参数指定我们要配置的顶点属性,顶点着色器中使用layout(location = 0)定义 89 第二个参数指定顶点属性的大小 90 第三个参数指定数据的类型 91 第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE,所有数据都会被映射到0(对于有符号型signed数据是-1)到1之间 92 第五个参数步长(Stride),它告诉我们在连续的顶点属性组之间的间隔 93 最后一个参数的类型是void*,数据在缓冲中起始位置的偏移量(Offset) 94 */ 95 glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0); 96 glEnableVertexAttribArray(0);//启用顶点属性layout(location = 0),顶点属性默认是禁用的 97 glBindBuffer(GL_ARRAY_BUFFER, 0);//设置完属性,解绑VBO 98 99 glBindVertexArray(0);//配置完VBO及其属性,解绑VAO 100 101 102 //绘制模式为线条GL_LINE,填充面GL_FILL 103 //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);//正反面 104 105 while (!glfwWindowShouldClose(window)) 106 { 107 processInput(window); 108 109 110 //清空屏幕 111 glClearColor(0.2f, 0.3f, 0.3f, 1.0f); 112 glClear(GL_COLOR_BUFFER_BIT); 113 114 115 //绘制物体 116 glUseProgram(shaderProgram);//激活程序对象 117 118 glBindVertexArray(VAO); 119 //使用VAO绘制 120 glDrawArrays(GL_TRIANGLES, 0, 3);//绘制图元为三角形,起始索引0,绘制顶点数量3 121 122 glfwSwapBuffers(window);//交换颜色缓冲(它是一个储存着GLFW窗口每一个像素颜色值的大缓冲) 123 glfwPollEvents();//检查有没有触发什么事件 124 } 125 126 //释放对象 127 glDeleteVertexArrays(1, &VAO); 128 glDeleteBuffers(1, &VBO); 129 130 std::cout << "finish!" << std::endl; 131 glfwTerminate();//释放/删除之前的分配的所有资源 132 return 0; 133 }
5. 绘制结果
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析