zoukankan      html  css  js  c++  java
  • OpenGL教程一

    引自:https://blog.csdn.net/u013654125/article/details/73613644

    GLEW, GLFW和GLM介绍

    现在你有了工程,就让我们开始介绍下工程所用到的开源库和为啥需要这些。

    The OpenGL Extension Wrangler (GLEW)是用来访问OpenGL 3.2 API函数的。不幸的是你不能简单的使用#include <GL/gl.h>来访问OpenGL接口,除非你想用旧版本的OpenGL。在现代OpenGL中,API函数是在运行时(run time)确定的,而非编译期(compile time)。GLEW可以在运行时加载OpenGL API。

    GLFW允许我们跨平台创建窗口,接受鼠标键盘消息。OpenGL不处理这些窗口创建和输入,所以就需要我们自己动手。我选择GLFW是因为它很小,并且容易理解。

    OpenGL Mathematics (GLM)是一个数学库,用来处理矢量和矩阵等几乎其它所有东西。旧版本OpenGL提供了类似glRotateglTranslateglScale等函数,在现代OpenGL中,这些函数已经不存在了,我们需要自己处理所有的数学运算。GLM能在后续教程里提供很多矢量和矩阵运算上帮助。

    在这系列的所有教程中,我们还编写了一个小型库tdogl用来重用C++代码。这篇教程会包含tdogl::Shadertdogl::Program用来加载,编译和链接shaders。

    什么是Shaders?

    Shaders在现代OpenGL中是个很重要的概念。应用程序离不开它,除非你理解了,否则这些代码也没有任何意义。

    Shaders是一段GLSL小程序,运行在GPU上而非CPU。它们使用OpenGL Shading Language (GLSL)语言编写,看上去像C或C++,但却是另外一种不同的语言。使用shader就像你写个普通程序一样:写代码,编译,最后链接在一起才生成最终的程序。

    Shaders并不是个很好的名字,因为它不仅仅只做着色。只要记得它们是个用不同的语言写的,运行在显卡上的小程序就行。

    在旧版本的OpenGL中,shaders是可选的。在现代OpenGL中,为了能在屏幕上显示出物体,shaders是必须的。

    为可能近距离了解shaders和图形渲染管线,我推荐Durian Software的相关文章The Graphics Pipeline chapter

     主程序Shader程序
    语言 C++ GLSL
    主函数 int main(int, char**); void main();
    运行于 CPU GPU
    需要编译?
    需要链接?

    那shaders实际上干了啥?这取决于是哪种shader。

    Vertex Shaders

    Vertex shader主要用来将点(x,y,z坐标)变换成不同的点。顶点只是几何形状中的一个点,一个点叫vectex,多个点叫vertices(发音为ver-tuh-seez)。在本教程中,我们的三角形需要三个顶点(vertices)组成。

    Vertex Shader的GLSL代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    
    #version 150
    
    in vec3 vert;
    
    void main() {
        // does not alter the vertices at all
        gl_Position = vec4(vert, 1);
    }
    

    第一行#version 150告诉OpenGL这个shader使用GLSL版本1.50.

    第二行in vec3 vert;告诉shader需要那一个顶点作为输入,放入变量vert

    第三行定义函数main,这是shader运行入口。这看上去像C,但GLSL中main不需要带任何参数,并且不用返回void。

    第四行gl_Position = vec4(vert, 1);将输入的顶点直接输出,变量gl_Position是OpenGL定义的全局变量,用来存储vertex shader的输出。所有vertex shaders都需要对gl_Position进行赋值。

    gl_Position是4D坐标(vec4),但vert是3D坐标(vec3),所以我们需要将vert转换为4D坐标vec4(vert, 1)。第二个的参数1是赋值给第四维坐标。我们会在后续教程中学到更多关于4D坐标的东西。但现在,我们只要知道第四维坐标是1即可,i可以忽略它就把它当做3D坐标来对待。

    Vertex Shader在本文中没有做任何事,后续我们会修改它来处理动画,摄像机和其它东西。

    Fragment Shaders

    Fragment shader的主要功能是计算每个需要绘制的像素点的颜色。

    一个”fragment”基本上就是一个像素,所以你可以认为片段着色器(fragment shader)就是像素着色器(pixel shader)。在本文中每个片段都是一像素,但这并不总是这样的。你可以更改某个OpenGL设置,以便得到比像素更小的片段,之后的文章我们会讲到这个。

    本文所使用的fragment shader代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    
    #version 150
    
    out vec4 finalColor;
    
    void main() {
        //set every drawn pixel to white
        finalColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
    

    再次,第一行#version 150告诉OpenGL这个shader使用的是GLSL 1.50。

    第二行finalColor = vec4(1.0, 1.0, 1.0, 1.0);将输出变量设为白色。vec4(1.0, 1.0, 1.0, 1.0)是创建一个RGBA颜色,并且红绿蓝和alpha都设为最大值,即白色。

    现在,就能用shader在OpenGL中绘制出了纯白色。在之后的文章中,我们还会加入不同颜色和贴图。贴图就是你3D模型上的图像。

    编译和链接Shaders

    在C++中,你需要对你的.cpp文件进行编译,然后链接到一起组成最终的程序。OpenGL的shaders也是这么回事。

    在这篇文章中用到了两个可复用的类,是用来处理shaders的编译和链接:tdogl::Shadertdogl::Program。这两个类代码不多,并且有详细的注释,我建议你阅读源码并且去链接OpenGL是如何工作的。

    什么是VBO和VAO?

    当shaders运行在GPU,其它代码运行在CPU时,你需要有种方式将数据从CPU传给GPU。在本文中,我们传送了一个三角的三个顶点数据,但在更大的工程中3D模型会有成千上万个顶点,颜色,贴图坐标和其它东西。

    这就是我们为什么需要Vertex Buffer Objects (VBOs)和Vertex Array Objects (VAOs)。VBO和VAO用来将C++程序的数据传给shaders来渲染。

    在旧版本的OpenGL中,是通过glVertexglTexCoordglNormal函数把每帧数据发送给GPU的。在现代OpenGL中,所有数据必须通过VBO在渲染之前发送给显卡。当你需要渲染某些数据时,通过设置VAO来描述该获取哪些VBO数据推送给shader变量。

    Vertex Buffer Objects (VBOs)

    第一步我们需要从内存里上传三角形的三个顶点到显存中。这就是VBO该干的事。VBO其实就是显存的“缓冲区(buffers)” - 一串包含各种二进制数据的字节区域。你能上传3D坐标,颜色,甚至是你喜欢的音乐和诗歌。VBO不关心这些数据是啥,因为它只是对内存进行复制。

    Vertex Array Objects (VAOs)

    第二步我们要用VBO的数据在shaders中渲染三角形。请记住VBO只是一块数据,它不清楚这些数据的类型。而告诉OpenGL这缓冲区里是啥类型数据,这事就归VAO管。

    VAO对VBO和shader变量进行了连接。它描述了VBO所包含的数据类型,还有该传递数据给哪个shader变量。在OpenGL所有不准确的技术名词中,“Vertex Array Object”是最烂的一个,因为它根本没有解释VAO该干的事。

    你回头看下本文的vertex shader(在文章的前面),你就能发现我们只有一个输入变量vert。在本文中,我们用VAO来说明“hi,OpenGL,这里的VBO有3D顶点,我想要你在vertex shader时,发三个顶点数据给vert变量。”

    在后续的文章中,我们会用VAO来说“hi,OpenGL,这里的VBO有3D顶点,颜色,贴图坐标,我想要你在shader时,发顶点数据给vert变量,发颜色数据给vertColor变量,发贴图坐标给vertTexCoord变量。”

    代码解释

    打开main.cpp,我们从main()函数开始。

    首先,我们初始化GLFW:

    1
    2
    3
    
    glfwSetErrorCallback(OnError);
    if(!glfwInit())
        throw std::runtime_error("glfwInit failed");
    

    glfwSetErrorCallback(OnError)这一行告诉GLFW当错误发生时调用OnError函数。OnError函数会抛一个包含错误信息的异常,我们能从中发现哪里出错了。

    然后我们用GLFW创建一个窗口。

    1
    2
    3
    4
    5
    6
    7
    8
    
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2);
    glfwWindowHint(GLFW_RESIZABLE, GL_FALSE);
    gWindow = glfwCreateWindow((int)SCREEN_SIZE.x, (int)SCREEN_SIZE.y, "OpenGL Tutorial", NULL, NULL);
    if(!gWindow)
        throw std::runtime_error("glfwCreateWindow failed. Can your hardware handle OpenGL 3.2?");
    

    该窗口包含一个向前兼容的OpenGL 3.2内核上下文。假如glfwCreateWindow失败了,你应该降低OpenGL版本。

    创建窗口最后一步,我们应该设置一个“当前”OpenGL上下文给刚创建的窗口:

    1
    
    glfwMakeContextCurrent(gWindow);
    

    无论我们调用哪个OpenGL函数,都会影响到“当前上下文”。我们只会用到一个上下文,所以设置完后,就别管它了。理论上来说,我们可以有多个窗口,且每个窗口都可以有自己的上下文。

    现在我们窗口有了OpenGL上下文变量,我们需要初始化GLEW以便访问OpenGL接口。

    1
    2
    3
    
    glewExperimental = GL_TRUE; //stops glew crashing on OSX :-/
    if(glewInit() != GLEW_OK)
        throw std::runtime_error("glewInit failed");
    

    这里的GLEW与OpenGL内核有点小问题,设置glewExperimental就可以修复,但希望再未来永远不要发生。

    我们也可以用GLEW再次确认3.2版本是否存在:

    1
    2
    
    if(!GLEW_VERSION_3_2)
        throw std::runtime_error("OpenGL 3.2 API is not available.");
    

    LoadShaders函数中,我们使用本教程提供的tdogl::Shadertdogl::Program两个类编译和链接了vertex shader和fragment shader。

    1
    2
    3
    4
    
    std::vector<tdogl::Shader> shaders;
    shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("vertex-shader.txt"), GL_VERTEX_SHADER));
    shaders.push_back(tdogl::Shader::shaderFromFile(ResourcePath("fragment-shader.txt"), GL_FRAGMENT_SHADER));
    gProgram = new tdogl::Program(shaders);
    

    LoadTriangle函数中,我们创建了一个VAO和VBO。这是第一步,创建和绑定新的VAO:

    1
    2
    
    glGenVertexArrays(1, &gVAO);
    glBindVertexArray(gVAO);
    

    然后我们创建和绑定新的VBO:

    1
    2
    
    glGenBuffers(1, &gVBO);
    glBindBuffer(GL_ARRAY_BUFFER, gVBO);
    

    接着,我们上传一些数据到VBO中。这些数据就是三个顶点,每个顶点包含三个GLfloat

    1
    2
    3
    4
    5
    6
    7
    
    GLfloat vertexData[] = {
        //  X     Y     Z
         0.0f, 0.8f, 0.0f,
        -0.8f,-0.8f, 0.0f,
         0.8f,-0.8f, 0.0f,
    };
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertexData), vertexData, GL_STATIC_DRAW);
    

    现在缓冲区包含了三角形的三个顶点,是时候开始设置VAO了。首先,我们应该启用shader程序中的vert变量。这些变量能被开启或关闭,默认情况下是关闭的,所以我们需要开启它。vert变量是一个“属性变量(attribute variable)”,这也是为何OpenGL函数名称中有带“Attrib”。我们可以在后续的文章中看到更多类型。

    1
    
    glEnableVertexAttribArray(gProgram->attrib("vert"));
    

    VAO设置最复杂的部分就是下个函数:glVertexAttribPointer。让我们先调用该函数,等会解释。

    1
    
    glVertexAttribPointer(gProgram->attrib("vert"), 3, GL_FLOAT, GL_FALSE, 0, NULL);
    

    第一个参数,gProgram->attrib("vert"),这就是那个需要上传数据的shder变量。在这个例子中,我们需要发数据给vertshader变量。

    第二个参数,3表明每个顶点需要三个数字。

    第三个参数,GL_FLOAT说明三个数字是GLfloat类型。这非常重要,因为GLdouble类型的数据大小跟它是不同的。

    第四个参数,GL_FALSE说明我们不需要对浮点数进行“归一化”,假如我们使用了归一化,那这个值会被限定为最小0,最大1。我们不需要对我们的顶点进行限制,所以这个参数为false。

    第五个参数,0,该参数可以在顶点之间有间隔时使用,设置参数为0,表示数据之间没有间隔。

    第六个参数,NULL,假如我们的数据不是从缓冲区头部开始的话,可以设置这个参数来指定。设置该参数为NULL,表示我们的数据从VBO的第一个字节开始。

    现在VBO和VAO都设置完成,我们需要对它们进行解绑定,防止一不小心被哪里给更改了。

    1
    2
    
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glBindVertexArray(0);
    

    到此,shader,VBO和VAO都准备好了。我们可以开始在Render函数里绘制了。

    首先,我们先清空下屏幕,让它变成纯黑色:

    1
    2
    
    glClearColor(0, 0, 0, 1); // black
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    

    然后告诉OpenGL我们要开始使用VAO和shader了:

    1
    2
    
    glUseProgram(gProgram->object());
    glBindVertexArray(gVAO);
    

    最后,我们绘制出三角形:

    1
    
    glDrawArrays(GL_TRIANGLES, 0, 3);
    

    调用glDrawArrays函数说明我们需要绘制三角形,从第0个顶点开始,有3个顶点被发送到shader。OpenGL会在当前VAO范围内确定该从哪里获取顶点。

    顶点将会从VBO中取出并发送到vertex shader。然后三角形内的每个像素会发送给fragment shader。接着fragment shader将每个像素变成白色。欢呼!

    现在绘制结束了,为了安全起见,我们需要将shader和VAO进行解绑定:

    1
    2
    
    glBindVertexArray(0);
    glUseProgram(0);
    

    最后一件事,在我们看到三角形之前需要切换帧缓冲:

    1
    
    glfwSwapBuffers(gWindow);
    

    在帧缓冲被交换前,我们会绘制到一个不可见的离屏(off-screen)帧缓冲区。当我们调用glfwSwapBuffers时,离屏缓冲会变成屏幕缓冲,所以我们就能在窗口上看见内容了。

    第一个OpenGL程序解读

    OpenGL中的大多数函数使用了一种基于状态的方法,大多数OpenGL对象都需要在使用前把该对象绑定到context上。这里有两个新名词——OpenGL对象和Context。

    Context

    Context是一个非常抽象的概念,我们姑且把它理解成一个包含了所有OpenGL状态的对象。如果我们把一个Context销毁了,那么OpenGL也不复存在。

    OpenGL对象

    我们可以把OpenGL对象理解成一个状态的集合,它负责管理它下属的所有状态。当然,除了状态,OpenGL对象还会存储其他数据。注意。这些状态和上述context中的状态并不重合,只有在把一个OpenGL对象绑定到context上时,OpenGL对象的各种状态才会映射到context的状态。因此,这时如果我们改变了context的状态,那么也会影响这个对象,而相反地,依赖这些context状态的函数也会使用存储在这个对象上的数据。

    因此,OpenGL对象的绑定既可能是为了修改该对象的状态(大多数对象需要绑定到context上才可以改变它的状态),也可能是为了让context渲染时使用它的状态。

    画了一个图,仅供理解。图中灰色的方块代表各种状态,箭头表示当把一个OpenGL对象绑定到context上后,对应状态的映射。

    前面提到过,OpenGL就是一个“状态机”。那些各种各样的API调用会改变这些状态,或者根据这些状态进行操作。但我们要注意的是,这只是说明了OpenGL是怎样被定义的,但硬件是否是按状态机实现的就是另一回事了。不过,这不是我们需要担心的地方。

    OpenGL对象包含了下面一些类型:Buffer Objects,Vertex Array Objects,Textures,Framebuffer Objects等等。我们下面会讲到Vertex Array Objects这个对象。

    这些对象都有三个相关的重要函数:

    1、负责生成一个对象的name。而name就是这个对象的引用。

    1. void glGen*(GLsizei n​, GLuint *objects​);  

    2、负责销毁一个对象

    1. void glDelete*(GLsizei n​, const GLuint *objects​);  

    3、将对象绑定到context上。

    1. void glBind*(GLenum target​, GLuint object​);  
    在开始第一个程序之前,我们还要了解一些图形名词。
     
    • 渲染(Rendering):计算机从模型到创建一张图像的过程。OpenGL仅仅是其中一个渲染系统。它是一个基于光栅化的系统,其他的系统还有光线追踪(但有时也会用到OpenGL)等。

    • 模型(Models)或者对象(Objects):这里两者的含义是一样的。指从几何图元——点、线、三角形中创建的东西,由顶点指定。

    • Shaders:这是一类特殊的函数,是在图形硬件上执行的。我们可以理解成,Shader是一些为图形处理单元(GPU)编译的小程序。OpenGL包含了编译工具来把我们编写的Shader源代码编译成可以在GPU上运行的代码。在OpenGL中,我们可以使用四种shader阶段。最常见的就是vertex shaders——它们可以处理顶点数据;以及fragment shaders,它们处理光栅化后生成的fragments。vertex shaders和fragment shaders是每个OpenGL程序必不可少的部分。

    • 像素(pixel):像素是我们显示器上的最小可见元素。我们系统中的像素被存储在一个帧缓存(framebuffer)中。帧缓存是一块由图形硬件管理的内存空间,用于供给给我们的显示设备。
     
     

    惊鸿一瞥

     
     
    我们的第一个程序(不完整)的运行结果如下:
     
     
     
    代码如下(提示:这里可以粗略地看下中文注释,后面会更详细讲述的):
    ///////////////////////////////////////////////////////////////////////
    //
    // triangles.cpp
    //
    ///////////////////////////////////////////////////////////////////////
    //--------------------------------------------------------------------
    //
    // 在程序一开头,我们包含了所需的头文件,
    // 声明了一些全局变量(但通常是不用全局变量在做的,这里只是为了说明一些基本问题)
    // 以及其他一些有用的程序结构
    //
    
    #include <iostream>
    using namespace std;
    
    #include "vgl.h"
    #include "LoadShaders.h"
    
    enum VAO_IDs { Triangles, NumVAOs };
    enum Buffer_IDs { ArrayBuffer, NumBuffers };
    enum Attrib_IDs { vPosition = 0 };
    
    GLuint  VAOs[NumVAOs];
    GLuint  Buffers[NumBuffers];
    
    const GLuint NumVertices = 6;
    
    //---------------------------------------------------------------------
    //
    // init
    //
    // init()函数用于设置我们后面会用到的一些数据.例如顶点信息,纹理等
    //
    
    void init(void) {
        glGenVertexArrays(NumVAOs, VAOs);
        glBindVertexArray(VAOs[Triangles]);
    
        // 我们首先指定了要渲染的两个三角形的位置信息.
        GLfloat  vertices[NumVertices][2] = {
            { -0.90, -0.90 },  // Triangle 1
            {  0.85, -0.90 },
            { -0.90,  0.85 },
            {  0.90, -0.85 },  // Triangle 2
            {  0.90,  0.90 },
            { -0.85,  0.90 }
        };
    
        glGenBuffers(NumBuffers, Buffers);
        glBindBuffer(GL_ARRAY_BUFFER, Buffers[ArrayBuffer]);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices),
                         vertices, GL_STATIC_DRAW);
    
        // 然后使用了必需的vertex和fragment shaders
        ShaderInfo  shaders[] = {
                { GL_VERTEX_SHADER, "triangles.vert" },
                { GL_FRAGMENT_SHADER, "triangles.frag" },
                { GL_NONE, NULL }
        };
    
        // LoadShaders()是我们自定义(这里没有给出)的一个函数,
        // 用于简化为GPU准备shaders的过程,后面会详细讲述
        GLuint program = LoadShaders(shaders);
        glUseProgram(program);
        // 最后这部分我们成为shader plumbing,
        // 我们把需要的数据和shader程序中的变量关联在一起,后面会详细讲述
        glVertexAttribPointer(vPosition, 2, GL_FLOAT,
                              GL_FALSE, 0, BUFFER_OFFSET(0));
        glEnableVertexAttribArray(vPosition);
    }
    
    //---------------------------------------------------------------------
    //
    // display
    //
    // 这个函数是真正进行渲染的地方.它调用OpenGL的函数来请求数据进行渲染.
    // 几乎所有的display函数都会进行下面的三个步骤.
    //
    
    void display(void) {
        // 1. 调用glClear()清空窗口
        glClear(GL_COLOR_BUFFER_BIT);
    
        // 2. 发起OpenGL调用来请求渲染你的对象
        glBindVertexArray(VAOs[Triangles]);
        glDrawArrays(GL_TRIANGLES, 0, NumVertices);
    
        // 3. 请求将图像绘制到窗口
        glFlush();
    }
    
    //---------------------------------------------------------------------
    //
    // main
    //
    // main()函数用于创建窗口,调用init()函数,最后进入到事件循环(event loop).
    // 这里仍会看到一些以gl开头的函数,但和上面的有所不同.
    // 这些函数来自第三方库,以便我们可以在不同的系统中更方便地使用OpenGL.
    // 这里我们使用的是GLUT和GLEW.
    //
    
    int main(int argc, char** argv) {
        glutInit(&argc, argv);
        glutInitDisplayMode(GLUT_RGBA);
        glutInitWindowSize(512, 512);
        glutInitContextVersion(4, 3);
        glutInitContextProfile(GLUT_CORE_PROFILE);
        glutCreateWindow(argv[0]);
    
        if (glewInit()) {
            cerr << "Unable to initialize GLEW ... exiting" << endl; exit(EXIT_FAILURE);
        }
        init();
    
        glutDisplayFunc(display);
    
        glutMainLoop();
    }
  • 相关阅读:
    vue vmodel input type=checkbox的问题
    springboot配置文件优先级
    原生js实现复制功能
    Long.valueOf和Long.parseLong的区别
    程序员学习参考
    国外开源项目
    .NET快速入门教程
    Microsoft Update Catalog 离线安装包下载
    php header示例代码
    CentOS下iptables设置
  • 原文地址:https://www.cnblogs.com/Anita9002/p/9145079.html
Copyright © 2011-2022 走看看