首页 > 其他分享 >OpenGL入门——第一个三角形

OpenGL入门——第一个三角形

时间:2023-08-27 17:33:23浏览次数:33  
标签:入门 OpenGL 像素 坐标 顶点 三角形 GL 着色器

一、渲染管线

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

 

标签:入门,OpenGL,像素,坐标,顶点,三角形,GL,着色器
From: https://www.cnblogs.com/ping-code/p/17660534.html

相关文章

  • 【Ehcache技术专题】「入门到精通」带你一起从零基础进行分析和开发Ehcache框架的实战
    Ehcache的存储方式Ehcache中对于缓存的存储主要有三种方式:分别是堆内存、非堆内存和磁盘。其中非堆内存是针对于企业版Ehcache才有的功能,它可以不受JavaGC的影响,能够创建很大的缓存。堆内存(MemoryStore)我们通常所有的MemoryStore实际上就是堆内存存储。MemoryStore总是可用的,所有......
  • kubernetes client-go快速入门及源码阅读
    client-go是kubernetes官方维护的一个go语言客户端,用于与k8s集群交互,使用client-go可以很方便的完成k8s的二次开发(似乎也必不可少),无论是稳定性还是健壮性都有充分的保障。client-go代码版本:v0.20.2个人水平有些,一定会出现不严谨或者错误的地方,如有错误麻烦评论指正,谢谢版......
  • Android入门教程 | Fragment 基础概念
    什么是Fragment?Fragment,直译为“碎片”,“片段”。Fragment表示FragmentActivity中的行为或界面的一部分。可以在一个Activity中组合多个片段,从而构建多窗格界面,并在多个Activity中重复使用某个片段。可以将片段视为Activity的模块化组成部分,它具有自己的生命周期,能接收自......
  • Arthas简单入门
    简介Arthas是一款线上监控诊断产品,通过全局视角实时查看应用load、内存、gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断,包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升线上问题排查效率。当你遇到以下类似问题而束手无策时,Arthas......
  • java入门
    java入门1、java语言简介1、java语言优点使用最广泛,且简单易学java是一门强类型语言java是非常完善的异常处理机制java提供了对大数据的基础性的支持2、java语言特点开源:java源代码是开饭的。跨平台:用java编写的代码或程序,可以在不同的操作系统上运行。JV......
  • 网关快速入门
            ......
  • SELinux 入门 pt.2
    哈喽大家好,我是咸鱼在《SELinux入门pt.1》中,咸鱼向各位小伙伴介绍了SELinux所使用的MAC模型、以及几个重要的概念(主体、目标、策略、安全上下文)我们还讲到:对于受SELinux管制的进程,会先检查SELinux策略规则,然后再检查DAC规则对于不受SELinux管制的进程,仍然会执......
  • Elasticsearch 保姆级入门篇
    Elasticsearch是一个分布式的、面向生产规模工作负载优化的搜索引擎。Kibana可以将Elasticsearch中的数据转化为直观的图表、图形和仪表盘。这篇文章,您将学习本地安装Elasticsearch和Kibana,以及使用开发工具/JavaSDK创建索引和搜索数据。1本地安装1.1创建网络......
  • Python入门第四天
    今日内容概要字符串——列表——字典——元组——集合——布尔——与用户交互——格式化输出数据类型之字符串(str)作用:用来记录人的名字,家庭住址,性别等描述性质的状态定义: 方式一: username='kevin'#单引号      方式二:username="kevin"#双引号方式......
  • 软件测试从入门到精通
    一、测试介绍软件测试概念使用技术手段验证软件是否满足要求测试主流技能1、功能测试2、自动化测试3、接口测试4、性能测试主流方向: 1、功能+接口测试 2、自动化+接口 3、功能+性能二、测试常用分类2.1阶段划分单元测试针对程序源代码进行测试(单元:最小独......