📜  使用OpenGL渲染三角形(使用着色器)

📅  最后修改于: 2021-06-01 02:05:30             🧑  作者: Mango

在本文中,我们将看到如何使用OpenGL渲染三角形。三角形可能是在点和线之后您可以在OpenGL中绘制的最简单的形状,并且您将创建的任何复杂几何形状都由连接在一起的三角形数量组成。
我们将使用可编程管线,因此我们还将编写简单的着色器程序,并将其编译并在以后用于渲染。现在这会使我们的程序有点冗长,但是很酷的事情是我们只执行了一次,然后重用我们已经编写的代码。实际上,对于大多数OpenGL代码而言,这都是正确的,因此,我们倾向于将大多数OpenGL代码编写为不同的函数,并在以后多次重用。
我们将为此使用的库是Glew和Glut和Ç++作为我们的编程语言。
搭建环境

  1. 下载最新的glew和glut标头,库和dll文件。
  2. 在Visual Studio中,创建一个Win 32控制台应用程序项目。
  3. 转到您的项目属性。
  4. 导入并包括所有配置的标头和库。
  5. 将dll文件放在您的源文件所在的位置。

完整的程序(程序后有解释)

CPP
// CPP program to render a triangle using Shaders
#include 
#include 
#include 
#include 
 
std::string vertexShader = "#version 430\n"
                           "in vec3 pos;"
                           "void main() {"
                           "gl_Position = vec4(pos, 1);"
                           "}";
 
std::string fragmentShader = "#version 430\n"
                             "void main() {"
                             "gl_FragColor = vec4(1, 0, 0, 1);"
                             "}";
 
// Compile and create shader object and returns its id
GLuint compileShaders(std::string shader, GLenum type)
{
 
    const char* shaderCode = shader.c_str();
    GLuint shaderId = glCreateShader(type);
 
    if (shaderId == 0) { // Error: Cannot create shader object
        std::cout << "Error creating shaders";
        return 0;
    }
 
    // Attach source code to this object
    glShaderSource(shaderId, 1, &shaderCode, NULL);
    glCompileShader(shaderId); // compile the shader object
 
    GLint compileStatus;
 
    // check for compilation status
    glGetShaderiv(shaderId, GL_COMPILE_STATUS, &compileStatus);
 
    if (!compileStatus) { // If compilation was not successful
        int length;
        glGetShaderiv(shaderId, GL_INFO_LOG_LENGTH, &length);
        char* cMessage = new char[length];
 
        // Get additional information
        glGetShaderInfoLog(shaderId, length, &length, cMessage);
        std::cout << "Cannot Compile Shader: " << cMessage;
        delete[] cMessage;
        glDeleteShader(shaderId);
        return 0;
    }
 
    return shaderId;
}
 
// Creates a program containg vertex and fragment shader
// links it and returns its ID
GLuint linkProgram(GLuint vertexShaderId, GLuint fragmentShaderId)
{
    GLuint programId = glCreateProgram(); // create a program
 
    if (programId == 0) {
        std::cout << "Error Creating Shader Program";
        return 0;
    }
 
    // Attach both the shaders to it
    glAttachShader(programId, vertexShaderId);
    glAttachShader(programId, fragmentShaderId);
 
    // Create executable of this program
    glLinkProgram(programId);
 
    GLint linkStatus;
 
    // Get the link status for this program
    glGetProgramiv(programId, GL_LINK_STATUS, &linkStatus);
 
    if (!linkStatus) { // If the linking failed
        std::cout << "Error Linking program";
        glDetachShader(programId, vertexShaderId);
        glDetachShader(programId, fragmentShaderId);
        glDeleteProgram(programId);
 
        return 0;
    }
 
    return programId;
}
 
// Load data in VBO and return the vbo's id
GLuint loadDataInBuffers()
{
    GLfloat vertices[] = { // vertex coordinates
                           -0.7, -0.7, 0,
                           0.7, -0.7, 0,
                           0, 0.7, 0
    };
 
    GLuint vboId;
 
    // allocate buffer space and pass data to it
    glGenBuffers(1, &vboId);
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
 
    // unbind the active buffer
    glBindBuffer(GL_ARRAY_BUFFER, 0);
 
    return vboId;
}
 
// Initialize and put everything together
void init()
{
    // clear the framebuffer each frame with black color
    glClearColor(0, 0, 0, 0);
 
    GLuint vboId = loadDataInBuffers();
 
    GLuint vShaderId = compileShaders(vertexShader, GL_VERTEX_SHADER);
    GLuint fShaderId = compileShaders(fragmentShader, GL_FRAGMENT_SHADER);
 
    GLuint programId = linkProgram(vShaderId, fShaderId);
 
    // Get the 'pos' variable location inside this program
    GLuint posAttributePosition = glGetAttribLocation(programId, "pos");
 
    GLuint vaoId;
    glGenVertexArrays(1, &vaoId); // Generate VAO
 
    // Bind it so that rest of vao operations affect this vao
    glBindVertexArray(vaoId);
 
    // buffer from which 'pos' will recive its data and the format of that data
    glBindBuffer(GL_ARRAY_BUFFER, vboId);
    glVertexAttribPointer(posAttributePosition, 3, GL_FLOAT, false, 0, 0);
 
    // Enable this attribute array linked to 'pos'
    glEnableVertexAttribArray(posAttributePosition);
 
    // Use this program for rendering.
    glUseProgram(programId);
}
 
// Function that does the drawing
// glut calls this function whenever it needs to redraw
void display()
{
    // clear the color buffer before each drawing
    glClear(GL_COLOR_BUFFER_BIT);
 
    // draw triangles starting from index 0 and
    // using 3 indices
    glDrawArrays(GL_TRIANGLES, 0, 3);
 
    // swap the buffers and hence show the buffers
    // content to the screen
    glutSwapBuffers();
}
 
// main function
// sets up window to which we'll draw
int main(int argc, char** argv)
{
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE);
    glutInitWindowSize(500, 500);
    glutInitWindowPosition(100, 50);
    glutCreateWindow("Triangle Using OpenGL");
    glewInit();
    init();
    glutDisplayFunc(display);
    glutMainLoop();
    return 0;
}


输出
现在,当您运行该程序时,您应该在屏幕上出现一个红色三角形,其外观应类似于下图。

main()函数
主要函数设置我们的窗口。我们初始化过剩,毛刺,指定窗口大小,其在屏幕上的位置,指定显示模式(仅告诉窗口将使用哪种颜色空间并将使用单缓冲区或双缓冲区),设置窗口标题,设置回调方法该过剩将用于在此窗口上绘制内容,并最终使该窗口可见。
loadDataInBuffers()函数
一个三角形由三个坐标点组成。这些点(存储为浮点数组)需要发送到图形处理器内存。
我们引用系统内存中某个位置的方式相对简单,只需获取指向该内存位置的指针即可,但使用GPU的内存并不是那么简单。
我们通过称为缓冲对象的方法来做到这一点。
缓冲区对象是您将数据上传到V-RAM并在以后引用的方式。
通常,您需要遵循三个步骤才能将数据放入视频存储器。

  1. 为您的缓冲区对象生成一个无符号整数标识符。
  2. 将该缓冲区对象绑定到目标。
  3. 指定缓冲区的大小,并可以选择将数据加载到其中。

请记住,OpenGL期望顶点坐标在[-1,1]范围内。超出此范围的任何内容都会被裁剪。但是,实际上我们可以自由选择自己的坐标系,这是一种非常普遍的做法,即定义自己的坐标系并根据该坐标系定义对象,然后再将其更改为OpengGL坐标系。
但是在这篇文章中,我们将沿着更轻松的道路前进,并在[-1,1]范围内指定三角形的坐标。在这些范围内的FYI坐标称为标准化设备坐标(NDC)。
着色器
在整个渲染管线中,输入数据(此处为顶点坐标)经历各个阶段。我们使用称为着色器的东西控制这些阶段
着色器是在GPU上执行的程序。 OpenGL中的着色器是用一种称为GLSL(OpenGL着色语言)的特殊语言编写的,您会发现它与C和C++非常相似。着色器使您可以直接控制图形管道。 GLSL在2004年由OpenGL ARB正式包含在OpenGL 2.0内核中。
您一直需要的两个着色器是

  1. 顶点着色器:这些着色器对GPU处理的每个顶点执行一次(如果是三角形,它将执行3次)。因此,如果您的场景包含一百万个顶点,则顶点着色器将为每个顶点执行一次一百万次。顶点着色器的主要工作是计算场景中顶点的最终位置。通过将向量(该向量具有4个分量)写入一个特殊的输出变量gl_Position(仅在顶点着色器中可用)来实现。顶点着色器的输入是使用in类型限定符指定的,而输出是在变量之前具有out限定符的。输入和输出变量必须具有全局范围。
  2. 片段着色器(Fragment Shader):您的几何图形经过其他中间阶段之后,如果最终到达了栅格化器。光栅化器将您的几何图形划分为多个片段。然后,片段着色器将为您的几何体(或三角形)中的每个片段运行。片段着色器的工作是确定每个片段的最终颜色。它将最终颜色写入特殊的输出变量gl_FragColor(仅可用于片段着色器)。传递给gl_FragColor的值是一个包含该片段的rgba值的向量。

着色器代码存储为字符串全局变量。稍后,我们将这个变量链接到我们的openGL程序中的数据。顶点位置无需修改就传递给gl_Position。
注意:通常的做法是将着色器代码放在不同的文件中,然后将该文件的内容存储在字符串变量中。
编写完着色器后,您需要创建着色器对象,附加其对应的源代码,对其进行编译,并检查是否存在错误。
compileShaders()函数
该函数编译着色器代码,检查错误并创建着色器对象并返回其ID。
我们将为顶点和片段着色器调用一次此函数
linkProgram()函数
您为渲染而编写的着色器需要合并到一个着色器程序中。
此函数这些着色器对象合并到一个着色器程序中,将它们链接(为该程序创建可执行文件),并检查在此过程中可能发生的错误。
init()函数
它使用上述所有功能,并将所有内容组合在一起。
我们使用所谓的VAO(顶点数组对象)
VAO:顶点数组对象(VAO)是OpenGL对象,它存储提供顶点数据所需的所有状态。它存储顶点数据的格式以及提供顶点数据数组的缓冲区对象。请注意,VAO不会复制,冻结或存储引用缓冲区的内容,如果您更改现有VAO引用的缓冲区中的任何数据,则VAO用户将看到这些更改。
着色器程序为顶点着色器(aka顶点属性)和统一变量(变量的值在我们渲染的原语中不变)的每个输入变量分配一个位置,我们需要知道是否要将它们链接到它们的位置。数据源。
display()函数
请记住,我们尚未告诉GPU开始渲染。 display 函数是将此命令传递给GPU的原因。
当Glut需要在屏幕上绘制内容时,它会调用此回调函数。

想要从精选的最佳视频中学习并解决问题,请查看有关从基础到高级C++的C++基础课程以及有关语言和STL的C++ STL课程。要完成从学习语言到DS Algo等的更多准备工作,请参阅“完整面试准备课程”