OpenGL入门-画一个三角形(VS2019可运行源码)

2.画一个三角形

配置VS2019,加载常用的库,参考1.1:

链接: link.

2.1预先需要知道的知识

1、屏幕和窗口是2D像素数组,因此当我们描绘3D图形的时候,我们需要工具把3D坐标转变为适应屏幕的2D像素,这就是OpenGL需要完成的工作。

2、这部分工作主要有图形渲染管线来完成(Graphics Pipeline)

​ 第一部分把3D坐标转换为2D坐标

​ 第二部分把2D坐标转换为实际的有颜色的像素

3、2D坐标和像素的不同:2D坐标精确表示一个点在2D空间中的位置,而2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。

4、着色器:在渲染管线当中各自运行的一些小程序,在图形渲染管线中处理我们的数据

5、我们在接下来的代码中以数组的形式传递3个3D坐标作为图形渲染管线的输入,用来表示一个三角形

​ 这个数组叫做顶点数据(Vertex Data)。顶点数据是一系列顶点的集合,一个顶点(Vertex)是一个3D坐标的数据的集合。
OpenGL入门-画一个三角形(VS2019可运行源码)
6、顶点着色器:输入:一个单独的顶点 作用:把3D坐标转为另一种3D坐标,同时可以对顶点数据进行基本处理

7、图元装配(primitive Assembly):将顶点着色器输出的所有顶点作为输入,将所有的顶点装配成指定图元的形状

8、几何着色器(Geoetry Shader):接受图元装配的输出作为输入,通过产生新顶点构造出新的图元来生成其他形状

9、光栅化阶段(Rasterization Stage),把图元映射为最终屏幕上相应的像素。生成供片段着色器(Fragment Shader)使用的片段(Fragment)。在片段着色器运行之前会执行裁切(Clipping)。裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

10、片段着色器的主要目的是计算一个像素的最终颜色,通常,片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

2.2 顶点输入

OpenGL是3D图形库,因此我们输入的所有坐标都是3D坐标(x,y,z)的形式。

OpenGL仅当3D坐标在3个轴(x、y和z)上都为-1.0到1.0的范围内时才处理它。

所有在所谓的标准化设备坐标(Normalized Device Coordinates)范围内的坐标才会最终呈现在屏幕上(在这个范围以外的坐标都不会显示)。

创建数组,指定三角形的三个顶点:

GLfloat vertices[]={
-0.5f,-0.5f,0.0f,
0.5f,-0.5f,0.0f,
0.0f,0.5f,0.0f
};
//创建了一个GLfloat数组,因为这里渲染的是一个2D三角形,所以三个顶点的z坐标都为0

​ 定义了顶点数据后,顶点着色器接收数据在GPU上创建内存用于储存顶点数据,并解释内存发送给显卡。

通过VBO管理内存

VBO(Vertex Buffer Objects, VBO)顶点缓冲对象,可以一次性地发送一大批数据到显卡上,而不是每个顶点发送一次。

通过glGenBuffers函数和一个缓冲ID生成一个VBO对象

GLuint VBO;//声明一个GLuint变量,GLuint相当于GL里面类似C++的unsigned int
glGenBuffers(1, &VBO);  
//glGenBuffers 官方解释:generate buffer object names
//第一个参数是要生成的缓冲对象的数量,第二个是要输入用来存储缓冲对象名称的数组
//该函数会在buffers里返回n个缓冲对象的名称。
//在本程序中,我们只需要他返回一个数组

声明一个GLuint变量,然后使用glGenBuffers后,它就会把缓冲对象保存在vbo里

glGenBuffers()函数仅仅是生成一个缓冲对象的名称,这个缓冲对象并不具备任何意义,它仅仅是个缓冲对象,还不是一个顶点数组缓冲,它类似于C语言中的一个指针变量,我们可以分配内存对象并且用它的名称来引用这个内存对象

使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标

​ 顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER。

​ 我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上:

glBindBuffer(GL_ARRAY_BUFFER, VBO);  
//glBindBuffer 官方解释:bind a named buffer object
//第一个就是缓冲对象的类型,第二个参数就是要绑定的缓冲对象的名称,也就是我们在上一个函数里生成的名称VBO

​ 绑定对象的过程就像设置铁路的道岔开关,每一个缓冲类型中的各个对象就像不同的轨道一样,我们将开关设置为其中一个状态,那么之后的列车都会驶入这条轨道。

​ 从这一刻起,我们使用的任何(在GL_ARRAY_BUFFER目标上的)缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数,它会把之前定义的顶点数据复制到缓冲的内存中:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

​ glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的第一个参数是目标缓冲的类型:顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第二个参数指定传输数据的大小(以字节为单位);用一个简单的sizeof计算出顶点数据大小就行。第三个参数是我们希望发送的实际数据。

第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式:

  • GL_STATIC_DRAW :数据不会或几乎不会改变。(基本都是用这个)
  • GL_DYNAMIC_DRAW:数据会被改变很多。
  • GL_STREAM_DRAW :数据每次绘制时都会改变。

2.3顶点着色器

如果我们打算做渲染的话,我们需要至少设置一个顶点和一个片段着色器

#version 330 core
//声明OpenGL版本,必不可少	每个着色器都起始于一个版本声明。
layout (location = 0) in vec3 position;
//创建一个vec3输入变量position
//位置变量的属性位置值为0

void main()
{
    gl_Position = vec4(position.x, position.y, position.z, 1.0);
    //设置顶点着色器的输出,把位置数据赋值非预定义的gl_Position变量,
    //由于我们的输入是一个3分量的向量,我们必须把它转换为4分量的。
    //我们可以把vec3的数据作为vec4构造器的参数,同时把w分量设置为1.0f
    //(我们会在后面解释为什么)来完成这一任务
}

2.4 编译着色器

2.4.1创建着色器对象

GLuint vertexShader
vertexShader=glcreateShader(GL_VERTEX_SHADER);

2.4.2 将着色器源码附加到着色器对象上并编译

glshaderSource(vertexShader,1,&vertexShaderSource,NULL);
//第一个参数是要编译的着色器对象:VertexShader
//第二个参数是指定传递的源码字符串数量
//第三个参数是顶点着色器真正的源码
glCompileShader(vertexShader);

2.5片段着色器

片段着色器全是关于计算像素最后的颜色输出

2.5.1在OpenGL中定义颜色

在计算机图形中颜色被表示为有4个元素的数组:红色、绿色、蓝色和alpha(透明度)分度,缩写为RGBA。

颜色的每个分量的强度设置在0.0到1.0之间

#version 330 core

out vec4 color	//声明是一个4分量向量

void main()
{
	color=vec4(1.0f,0.5f,0.2f,1.0f);//橘黄色
	//alpha值为1.0代表完全不透明
}

2.5.2 编译片段着色器

使用GL_FRAGMENT_SHADER常量作为着色器类型

GLuint fragmentShader;//定义一个fragmentShader整形变量
fragmentShader = glCreateShader(GL_Fragment_SHADER);
//glCreateShader创建一个着色器对象,括号内的参数指定要创建的着色器类型
//只支持两种类型的着色器:GL_VERTEX_SHADER和GL_FRAGMENT_SHADER。
glShaderSource(fragmentShader,1,&fragmentShaderSource,null);
glCompileShader(fragmentShader);

GL_VERTEX_SHADER类型的着色器是一个用于在可编程顶点处理器上运行的着色器。 GL_FRAGMENT_SHADER类型的着色器是一个着色器,旨在在可编程片段处理器上运行。

2.6着色器程序

根据前面的步骤,顶点着色器和片段着色器都已经编译了。

着色器程序对象是多个着色器合并之后并最终链接完成的版本

创建一个程序对象

GLuint shaderProgram;
shaderProgram = glCreateProgram();
//glCreateProgram函数创建一个程序,并返回新创建程序对象ID引用

把之前编译的着色器附加到程序对象上,然后用函数链接它们

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

得到一个完整的着色器程序后,我们调用glUserProgram函数用刚创建的程序对象作为它的参数,以激活这个程序对象

glUseProgram(shaderProgram);

现在可以删除着色器对象:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

2.7 链接顶点属性

我们需要在渲染前指定OpenGL该如何解释顶点数据。

我们的顶点缓冲数据将被如下图解释:
OpenGL入门-画一个三角形(VS2019可运行源码)

  • 位置数据被储存为32-bit(4字节)浮点值

  • 每个位置包含3个这样的值

  • 在这3个值之间没有间隙(或其他值)。这几个值在数组中紧密排列

  • 数据中第一个值在缓冲开始的位置

2.7.1 glVertexAttribPointer函数

使用glVertexAttribPointer函数告诉OpenGL该如何解析顶点数据

glvertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,3*siezeof(GLfloat),(GLvoid*)0);

glVertexAttribPointer函数的参数:

第一个参数:指定我们要配置的顶点属性。

​ 在前面的顶点着色器中,我们使用了layout(location=0)定义了position顶点属性的位置值location,也就是把顶点属性的位置值设置了为0

OpenGL入门-画一个三角形(VS2019可运行源码)
第二个参数:指定顶点属性的大小

​ 这里的顶点属性是vec3,由3个值组成,所以大小是3

第三个参数:指定数据类型

​ vec*都是由浮点数值GL_FLOAT组成

第四个参数:决定数据是否被标准化。

​ TRUE则所有数据映射到0

第五个参数:输入在连续的顶点属性组之间的间隔。也就是一个顶点的内存大小。

​ 每个顶点的数据占据了3*sizeof(GLfloat)的大小

第六个参数:表示位置数据在缓冲中起始位置的偏移量(Offset)。(不太清楚实际意思。。。)

2.7.1 glEnableVertexAttribArray函数

glEnableVertexAttribArray(0);//以顶点属性位置值作为参数,启用顶点属性,顶点属性默认是禁用的

顶点数组对象(VAO)

VAO存储所有的顶点属性调用,当配置顶点属性指针时,只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。

只需要绑定不同的VAO就可以在不同的顶点数据和属性配置之间切换。

VAO中储存的内容:

  • glEnableVertexAttribArray和glDisableVertexAttribArray的调用。
  • 通过glVertexAttribPointer设置的顶点属性配置。
  • 通过glVertexAttribPointer调用进行的顶点缓冲对象与顶点属性链接。

VAO和VBO的关系图解

OpenGL入门-画一个三角形(VS2019可运行源码)

创建和使用VAO

Gluint VAO;
glGenVertexArrays(1,&VAO);//创建了一个VAO对象,与创建一个VBO对象类似

要想使用VAO,要做的只是使用glBindVertexArray绑定VAO。从绑定之后起,我们应该绑定和配置对应的VBO和属性指针,之后解绑VAO供之后使用。当我们打算绘制一个物体的时候,我们只要在绘制物体前简单地把VAO绑定到希望使用的设定上就行了。

使用glDrawArrays函数激活着色器绘制图形

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glBindVertexArray(0);  

glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过,我们希望绘制的是一个三角形,这里传递GL_TRIANGLES给它。第二个参数指定了顶点数组的起始索引,我们这里填0。最后一个参数指定我们打算绘制多少个顶点,这里是3(我们只从我们的数据中渲染一个三角形,它只有3个顶点长)。

完整源代码(在vs2019上可运行)

#include <iostream>
#include <GL/glew.h>
#include <GLFW/glfw3.h>
#include <GL/glut.h>


// Function prototypes
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode);

// Window dimensions
const GLuint WIDTH = 800, HEIGHT = 600;

// Shaders
const GLchar* vertexShaderSource = "#version 330 core\n"
"layout (location = 0) in vec3 position;\n"
"void main()\n"
"{\n"
"gl_Position = vec4(position.x, position.y, position.z, 1.0);\n"
"}\0";
const GLchar* fragmentShaderSource = "#version 330 core\n"
"out vec4 color;\n"
"void main()\n"
"{\n"
"color = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n\0";

// The MAIN function, from here we start the application and run the game loop
int main()
{
    // Init GLFW
    glfwInit();
    // Set all the required options for GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);

    // Create a GLFWwindow object that we can use for GLFW's functions
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, "LearnOpenGL", nullptr, nullptr);
    glfwMakeContextCurrent(window);

    // Set the required callback functions
    glfwSetKeyCallback(window, key_callback);

    // Set this to true so GLEW knows to use a modern approach to retrieving function pointers and extensions
    glewExperimental = GL_TRUE;
    // Initialize GLEW to setup the OpenGL Function pointers
    glewInit();

    // Define the viewport dimensions
    int width, height;
    glfwGetFramebufferSize(window, &width, &height);
    glViewport(0, 0, width, height);


    // Build and compile our shader program
    // Vertex shader
    GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    glCompileShader(vertexShader);
    // Check for compile time errors
    GLint success;
    GLchar 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;
    }
    // Fragment shader
    GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // Check for compile time errors
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::FRAGMENT::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
    // Link shaders
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    // Check for linking errors
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);


    // Set up vertex data (and buffer(s)) and attribute pointers
    GLfloat vertices[] = {
        -0.5f, -0.5f, 0.0f, // Left  
         0.5f, -0.5f, 0.0f, // Right 
         0.0f,  0.5f, 0.0f  // Top   
    };
    GLuint VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    // Bind the Vertex Array Object first, then bind and set vertex buffer(s) and attribute pointer(s).
    glBindVertexArray(VAO);

    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(GLfloat), (GLvoid*)0);
    glEnableVertexAttribArray(0);

    glBindBuffer(GL_ARRAY_BUFFER, 0); // Note that this is allowed, the call to glVertexAttribPointer registered VBO as the currently bound vertex buffer object so afterwards we can safely unbind

    glBindVertexArray(0); // Unbind VAO (it's always a good thing to unbind any buffer/array to prevent strange bugs)

    // Game loop
    while (!glfwWindowShouldClose(window))
    {
        // Check if any events have been activiated (key pressed, mouse moved etc.) and call corresponding response functions
        glfwPollEvents();

        // Render
        // Clear the colorbuffer
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // Draw our first triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO);
        glDrawArrays(GL_TRIANGLES, 0, 3);
        glBindVertexArray(0);

        // Swap the screen buffers
        glfwSwapBuffers(window);
    }
    // Properly de-allocate all resources once they've outlived their purpose
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    // Terminate GLFW, clearing any resources allocated by GLFW.
    glfwTerminate();
    return 0;
}

// Is called whenever a key is pressed/released via GLFW
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mode)
{
    if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
}
上一篇:Creating Dialogbased Windows Application (4) / 创建基于对话框的Windows应用程序(四)Edit Control、Combo Box的应用、Unicode转ANSI、Open File Dialog、文件读取、可变参数、文本框自动滚动 / VC++, Windows


下一篇:emlog在nginx中添加rewrite规则