02 绘制三角形

教程:LearnOpenGL中文版

可参考:https://zhuanlan.zhihu.com/p/107883746

1 Linux上的OpenGL库

即我们需要在编译成二进制的命令中,添加-lGL。如:

// 使用G++、gcc编译器时,使用的命令,我们后面需要带有参数‘lGL’
gcc -o test.out test.c -lGL

在Linux下你需要链接libGL.so库文件,这需要添加-lGL到你的链接器设置中。如果找不到这个库你可能需要安装Mesa,NVidia或AMD的开发包,这部分因平台而异(而且我也不熟悉Linux)就不仔细讲解了。

但是,我们还需要带其他参数。如,

  • 你使用的是GLFW库:-lGL -lGLEW -lglfw3 -lX11 -lpthread -lXrandr -lXi

    需添加头文件:#include <GLFW/glfw3.h>

  • 你使用的是GLAD库:

    需添加头文件:#include <glad/glad.h>

    注意,glad库的使用有些特殊,请参考:https://www.programmersought.com/article/8262677632/

2 图形渲染管线

在OpenGL中,任何事物都在3D空间中,而屏幕和窗口却是2D像素数组,这导致OpenGL的大部分工作都是关于把3D坐标转变为适应你屏幕的2D像素

  • 3D坐标转为2D坐标的处理过程是由OpenGL的图形渲染管线(Graphics Pipeline,大多译为管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程)管理的。

图形渲染管线可以被划分为两个主要部分:

  • 第一部分把你的3D坐标转换为2D坐标;(即,图形渲染管线接收的是3D坐标)
  • 第二部分是把2D坐标转变为实际的有颜色的像素。

注意:

2D坐标精确表示一个点在2D空间中的位置;

2D像素是这个点的近似值,2D像素受到你的屏幕/窗口分辨率的限制。

2.1 并行性

图形渲染管线可以被划分为几个阶段,每个阶段将会把前一个阶段的输出作为输入。所有这些阶段都是高度专门化的(它们都有一个特定的函数),并且很容易并行执行。

由于它们具有并行执行的特性,它们在GPU上为每一个(渲染管线)阶段运行各自的小程序,从而在图形渲染管线中快速处理你的数据。这些小程序叫做着色器(Shader)

2.2 着色器

OpenGL着色器是用OpenGL着色器语言(OpenGL Shading Language, GLSL)写成的。

有些着色器允许开发者自己配置,这就允许我们用自己写的着色器来替换默认的。这样就可更细致地控制图形渲染管线中的特定部分,而且因为它们运行在GPU上,所以它们可以给我们节约宝贵的CPU时间。

2.3 图形渲染管道的各个阶段

下面,你会看到一个图形渲染管线的每个阶段的抽象展示。要注意蓝色部分代表的是我们可以注入自定义的着色器的部分。

img

  • 顶点着色器(Vertex Shader):是图形渲染管线的第一个部分,它把一个单独的顶点作为输入。主要的目的是把3D坐标转为另一种3D坐标(后面会解释),同时顶点着色器允许我们对顶点属性进行一些基本处理。

  • 图元装配(Primitive Assembly)阶段:将顶点着色器输出的所有顶点作为输入(如果是GL_POINTS,那么就是一个顶点),并将所有的点装配成指定图元的形状;

    为了让OpenGL知道我们的坐标和颜色值构成的到底是什么,OpenGL需要你去指定这些数据所表示的渲染类型。我们是希望把这些数据渲染成一系列的点?一系列的三角形?还是仅仅是一个长长的线?做出的这些提示叫做图元(Primitive),任何一个绘制指令的调用都将把图元传递给OpenGL。这是其中的几个:GL_POINTSGL_TRIANGLESGL_LINE_STRIP

  • 几何着色器(Geometry Shader):图元装配阶段的输出会传递给几何着色器。几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。

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

  • 片段着色器(Fragment Shader):主要目的是计算一个像素的最终颜色,这也是所有OpenGL高级效果产生的地方。片段着色器包含3D场景的数据(比如光照、阴影、光的颜色等等),这些数据可以被用来计算最终像素的颜色。

    OpenGL中的一个片段是OpenGL渲染一个像素所需的所有数据。

  • Alpha测试和混合(Blending)阶段:在所有对应颜色值确定以后,最终的对象将会被传到最后一个阶段。

    • 这个阶段检测片段的对应的深度(和模板(Stencil))值。用它们来判断这个像素是其它物体的前面还是后面,决定是否应该丢弃。
    • 这个阶段也会检查alpha值(alpha值定义了一个物体的透明度)并对物体进行混合(Blend)。

图形渲染管线非常复杂,它包含很多可配置的部分。然而,对于大多数场合,我们只需要配置顶点和片段着色器就行了。几何着色器是可选的,通常使用它默认的着色器就行了。

注意:在现代OpenGL中,我们必须定义至少一个顶点着色器和一个片段着色器(因为GPU中没有默认的顶点/片段着色器)。

3 绘制三角形

3.1 绘制步骤

3.1.1 定义顶点数据数组

1、定义顶点数据数组

	float vertices[] = {
		// 第一个四边形
		0.1f, 0.9f, 0.0f,    
		0.9f, 0.9f, 0.0f,    
		0.9f, 0.1f, 0.0f,   
		0.1f, 0.1f, 0.0f,    
	};

    // 索引(非必须:当要绘制多个三角形时,我们可以使用索引)
    unsigned int indices[] = { // 注意索引从0开始! 
    0, 1, 3, // 第一个三角形
    1, 2, 3  // 第二个三角形
    };

3.1.2 VAO、VBO、EBO

讲解:

  • VBO:顶点缓冲对象(Vertex Buffer Objects, VBO)。当定义一些数据之后(如顶点数据),我们可以通过VBO管理这个内存,它会在GPU内存(显存)中存储大量的顶点;

    使用这些缓冲对象的好处是我们可以一次性的发送一大批数据到显卡上,而不是每个顶点发送一次。从CPU把数据发送到显卡相对较慢,所以只要可能我们都要尝试尽量一次性发送尽可能多的数据。当数据发送至显卡的内存中后,顶点着色器几乎能立即访问顶点,这是个非常快的过程。

  • VAO:顶点数组对象(Vertex Array Object, VAO)。可以像顶点缓冲对象那样被绑定,任何随后的顶点属性调用都会储存在这个VAO中。

    这样的好处就是,当配置顶点属性指针时,你只需要将那些调用执行一次,之后再绘制物体的时候只需要绑定相应的VAO就行了。这使在不同顶点数据和属性配置之间切换变得非常简单,只需要绑定不同的VAO就行了。刚刚设置的所有状态都将存储在VAO中

  • EBO:索引缓冲对象(Element Buffer Object,EBO,也叫Index Buffer Object,IBO)。EBO也是一个缓冲,它专门储存索引,OpenGL调用这些顶点的索引来决定该绘制哪个顶点。

    举个例子:假设我们不再绘制一个三角形而是绘制一个矩形。我们可以绘制两个三角形来组成一个矩形(OpenGL主要处理三角形)。这样就产生50%的额外开销。当我们有包括上千个三角形的模型之后这个问题会更糟糕,这会产生一大堆浪费。更好的解决方案是只储存不同的顶点,并设定绘制这些顶点的顺序。这样子我们只要储存4个顶点就能绘制矩形了,之后只要指定绘制的顺序就行了。

2.1、创建,绑定VAO (Vertex Array Object)顶点数组对象 管理顶点属性

  1. 创建VAO;glGenVertexArrays(1, &VAO);

  2. 绑定VAO;glBindVertexArray(VAO);

    如需多个VAO 创建VAO数组 VAOs[n],glGenVertexArrays(n,VAOs),并分别进行绑定 glBindVertexArray(VAOs[1]) ;

  3. 在2.2VBO中完成)把顶点数据拷贝到VBO缓冲内存中; glBufferData(…);

  4. 设置顶点属性指针:glVertexAttribPointer(…)

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

2.2、创建,绑定VBO(Vertex Buffer Objects)顶点缓冲对象 管理顶点数据储存的内存

  1. 创建VBO; glGenBuffer(1,&VBO);

  2. 绑定VBO; glBindBuffer(GL_ARRAY_BUFFER,VBO);

    (OpenGL允许我们同时绑定多个缓冲,只要它们是不同的缓冲类型。我们可以使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上)

  3. 把顶点数据拷贝到VBO缓冲内存中; glBufferData(…);

    如需多个VBO 创建VBO数组 VBOs[n];glGenVertexArrays(n,VBOs),并分别进行绑定 glBindBuffer(GL_ARRAY_BUFFER, VBOs[1]) ;

    img

2.3、(可选项)创建,绑定EBO(ElementBuffer Objects/Index Buffer Object)索引缓冲对象 管理顶点数据储存的内存

  1. 创建EBO;glGenBuffers(1, &EBO);

  2. 绑定EBO;glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);

    如需多个EBO 创建EBO数组 EBOs[n];glGenBuffers(n,EBOs),并分别进行绑定 glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBOs[1]) ;

img

3.1.2 着色器的定义、创建、编译、删除

3、定义顶点着色器创建顶点着色器对象编译顶点着色器

3.1 使用GLSL语言来定义顶点着色器

  1. 声明所用的OpenGL版本号以及模块;#Version 330 core;
  2. 声明用来接收顶点数据的变量以及位置;in vec3 aPos/Location;
  3. 简单的赋值;预定义变量gl_Position;

3.2 创建顶点着色器对象

  1. 创建顶点着色器对象;glCreateShader(CL_VERTEX_SHADER);

3.3 编译顶点着色器

  1. 把定义的着色器源码附加到顶点着色器对象上;glShaderSource(…);
  2. 编译着色器;glCompileShader(VertexShader);
  3. 检查是否编译成功,并打印信息;glGetShaderriv();

3、定义片源着色器创建顶点着色器对象编译片源着色器

3.1 使用GLSL语言来定义片源着色器

  1. 声明所用的OpenGL版本号以及模块;#Version 330 core;
  2. 声明用来输出颜色的变量;out vec4 FragColor;
  3. 简单的赋值;预定义变量FragColor ;

3.2 创建顶点着色器对象

  1. 创建片源着色器对象;glCreateShader(GL_FRAGMENT_SHADER);

3.3 编译片源着色器

  1. 把定义片源着色器源码附加到片源着色器对象上;glShaderSource(…);
  2. 编译着色器;glCompileShader(fragmentShader);
  3. 检查是否编译成功,并打印信息;glGetShaderriv();

3.1.3 链接 着色器程序

当着色器都编译完之后,还需要把着色器对象 链接 到一个用来渲染的**着色器程序(Shader Program)**中。

着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本如果要使用刚才编译的着色器我们必须把它们链接(Link)为一个着色器程序对象,然后在渲染对象的时候激活这个着色器程序已激活着色器程序的着色器将在我们发送渲染调用的时候被使用。

当链接着色器至一个程序的时候,它会把每个着色器的输出链接到下个着色器的输入。当输出和输入不匹配的时候,你会得到一个连接错误。

4、链接着色器程序

  1. 创建链接着色程序;glCreateProgram();
  2. 把之前编译好的那些着色器 绑定到链接程序上;glAttachShader(…);
  3. 链接编译;glLinkProgram(shaderProgram);
  4. 检查链接编译是否成功,并打印信息;glGetProgramInfoLog(…);
  5. 激活程序对象(一般放在渲染循环里);glUseProgram(shaderProgram);
  6. 渲染绘制;glDrawArrays(…);
  7. 删除创建的着色器;glDeleteShader(vertexShader/fragmentShader);

3.1.4 删除着色器对象

5、删除着色器对象:在把着色器对象链接到程序对象以后,记得删除着色器对象,我们不再需要它们了:

  1. glDeleteShader(vertexShader);
  2. glDeleteShader(fragmentShader);

⭐ 绘制

绘制部分我们是在“01创建窗口”一文中,的窗口创建步骤的第6步:创建渲染循环中进行绘制:

  1. 激活程序对象:首先使用glUseProgram函数,用刚创建的 程序对象shaderProgram 作为它的参数,以激活这个程序对象:

  2. 绘制物体

    1. OpenGL提供glDrawArrays函数,它使用当前激活的着色器、之前定义的顶点属性配置,和VBO的顶点数据(通过VAO间接绑定)来绘制图元。

      在glUseProgram函数调用之后,每个着色器调用和渲染调用都会使用这个程序对象(也就是之前写的着色器)了。

    2. OpenGL提供glDrawElements函数,其意思是使用当前绑定的索引缓冲对象中的索引进行绘制

/********************************* 1. 只绘制1个三角形,此时无需EBO****************************************/
// https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.1.hello_triangle/hello_triangle.cpp
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        
        // 开始绘制
        // draw our first triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        glDrawArrays(GL_TRIANGLES, 0, 3);
        // glBindVertexArray(0); // no need to unbind it every time 
 
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

/********************************* 1. 绘制多个三角形,此时使用EBO****************************************/
// https://learnopengl.com/code_viewer_gh.php?code=src/1.getting_started/2.2.hello_triangle_indexed/hello_triangle_indexed.cpp
    // render loop
    // -----------
    while (!glfwWindowShouldClose(window))
    {
        // input
        // -----
        processInput(window);

        // render
        // ------
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 开始绘制
        // draw our first triangle
        glUseProgram(shaderProgram);
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
        //glDrawArrays(GL_TRIANGLES, 0, 6);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //⭐
        // glBindVertexArray(0); // no need to unbind it every time 
 
        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        // -------------------------------------------------------------------------------
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

3.1.5 释放资源

6、optional: de-allocate all resources once they’ve outlived their purpose:

    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);
    glDeleteProgram(shaderProgram); // 将着色器程序也释放

3.2 相关函数

请查看原文:

  1. https://learnopengl-cn.github.io/01%20Getting%20started/04%20Hello%20Triangle/#_11
  2. https://zhuanlan.zhihu.com/p/107883746(辅助查看)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值