ping-code

导航

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

统计

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. 绘制结果

 

posted on   一只小瓶子  阅读(398)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示