现代OpenGL(一):我的第一个OpenGL程序

OpenGL简介

OpenGL是一种应用程序编程接口(Application Programming Interface,API)它是一种可以对图形硬件设备特征进行访问的软件库。 在OpenGL 3.0以前的版本或者使用兼容模式的OpenGL环境,OpenGL包含一个固定管线(fixed-function pipeline),它可以在不使用着色器的环境下处理几何与像素数据。我们看到的glBegin()、glRectf()以及glEnd()这些函数都是以前固定管线模式中所使用的API函数。 从3.1版本开始,固定管线从核心模式中去除,因此我们必须使用着色器来完成工作。现代OpenGL渲染管线严重依赖着色器来处理传入的数据,我们一般会使用GLSL(OpenGL Shading Language)编写着色器程序,GLSL语法类似于C语言,GLSL编译以后运行在GPU端。

OpenGL的可编程管线包含如下过程(下图来自OpenGL红宝书《OpenGL Programming Guide》第八版):

可以看到从开始的顶点数据到最后在界面上的显示需要经过很多过程,这里我比较重要的是和必经的阶段包括Vertex Shader(顶点着色阶段)、Rasterization(光栅化阶段)和Frgament Shader(片元着色阶段)。 下面的图详细说明了这几个几段内部图形的处理与变化《摘自https://open.gl/drawing》:

顶点着色阶段将接受你在顶点缓存对象中给出的顶点数据,独立处理每个顶点。这个阶段对于所有的OpenGL程序都是必需的,而且必需绑定一个着色器。 光栅化就是把顶点数据转换为片元的过程。片元中的每一个元素对应于帧缓冲区中的一个像素。 片元着色阶段会处理OpenGL光栅化之后生成的独立片元,并且这个阶段也必需绑定一个着色器。 总结一下: 一个用来渲染图像的OpenGL程序需要执行的主要操作如下: 1. 从OpenGL的几何图元中设置数据,用于构建形状。 2. 使用不同的着色器(shader)对输入的图元数据执行计算操作,判断它们的位置、颜色,以及其他渲染属性。 3. 将输入图元的数学描述转化为与屏幕位置对应的像素片元(fragment)。这一步也称为光栅化(rasterization)。 4. 最后,针对光栅化过程产生的每个片元,执行片元着色器(fragment shader),从而决定这个片元的最终颜色和位置。 5. 如果有必要,还需要对每个片元执行一些额外的操作,例如判断片元对应的对象是否可见,或者将片元的颜色与当前屏幕位置的颜色进行融合。


OpenGL开发环境搭建

说了OpenGL的基本原理,下面来看看开发现代OpenGL程序需要准备的前期开发环境。 在我的上篇博文《OpenGL+Visual Studio 2010开发环境搭建 》中提到: OpenGL主要由以下库函数组成: OpenGL核心库:包含115个最基本的命令函数,它们都是以”gl“为前缀,可以在任何OpenGL的工作平台上应用。这部分函数用于常规的、核心的图形处理。 OpenGL实用库函数:包含43个函数,以”glu“作为前缀,在任何OpenGL平台上都可以应用。这部分函数通过调用核心库的函数来实现一些复杂的操作。 OpenGL辅助库函数:OpenGL Utility Toolkit (GLUT)包含31个函数,以”aux“作为前缀,但它们不能在所有的OpenGL平台上使用。OpenGL的辅助库函数主要用于窗口管理、输入输出处理以及绘制一些简单的三维形体。

其实GLUT主要用于窗口管理、输入输出处理以及绘制一些简单的三维形体。而且GLUT不是开源的,所以现在有很多GLUT的替代库,比如GLUT的开源版本Freeglut和OpenGLUT。在https://open.gl/context这个教程中,作者提到了三个用于取代GLUT的第三方库:SFML、SDL、GFLW有兴趣的朋友可以自己Google一下这些库。由于SFML(Simple and Fast Multimedia Library)是使用C++编写的,我本人比较喜欢使用C++而非C语言,所以下面的示例程序会使用SFML库。

此外,还需要介绍一个库GLEW(OpenGL Extension Wrangler)。GLEW是一个跨平台的C++扩展库,基于OpenGL图形接口。GLEW能自动识别你的平台所支持的全部OpenGL高级扩展涵数。如果没有GLEW,我们可能还需要执行相当多的工作才能够运行程序。


第三方库的配置

由于我们这里需要用到好些第三方库,这里顺便说一下在Visual Studio中如何使用第三方的C++库。 首先,下载官方提供的库文件并解压,有的只提供了源文件,需要我们自己编译。一般的至少都会包含三个目录:include文件夹、lib文件夹和bin文件夹。include文件夹里面包含了我们所需要的头文件;lib文件夹中有的会提供静态链接库,有的会提供动态链接所用的链接库文件(Windows下特有的);bin文件夹下是动态链接库(Windows下是dll文件,Linux下是so文件)。当然还可能会有一些其他文件。 然后,我们在Visual Studio中新建一个C++工程,并且新建一个C++源文件(cpp文件)。在工程上右键Properties,我习惯在C/C++→General→Additional Include Directories中添加库的include目录,将头文件包含进来。接下来在Linker→General→Additional Library Directories中添加lib库所在目录,在Linker→Input→Additional Dependencies中添加所需要的dll文件(这里需要添加那些dll文件和你程序中所要使用到的函数功能有关)。 最后,记得将库文件所在的bin目录添加到你的path环境变量中。Windows下在高级系统设置→环境变量中进行设置。

所以,怎么使用SFML和GLEW库应该不用多说了吧!如果有朋友遇到问题了,可以百度其它博客,上面应该有更详细的介绍或者说明。


HelloWorld示例程序

下面我们新建一个C++控制台程序,然后再新建一个cpp文件,配置好需要的SFML和GLEW库,开始编写代码。 这里我们需要配置的链接库文件包括: opengl32.lib glu32.lib glew32.lib sfml-system-d.lib sfml-window-d.lib

#include <GL/glew.h>
#include <SFML/Window.hpp>

#define GLSL(src) "#version 150 core\n" #src

// Vertex渲染器代码片段
const GLchar* vertexSource = GLSL(
    in vec2 position;
    in vec3 color;
    out vec3 mColor;
    void main() {
        mColor = color;
        gl_Position = vec4(position, 0.0f, 1.0f);
    }
);

// Fragment渲染器片段
const GLchar* fragmentSource = GLSL(
    in vec3 mColor;
    out vec4 oColor;
    void main() {
        oColor = vec4(mColor, 1.0f);
    }
);

// 创建指定类型的Shader
GLuint createShader(GLenum type, const GLchar* src)
{
    GLuint shader = glCreateShader(type);
    glShaderSource(shader, 1, &src, nullptr);
    glCompileShader(shader);
    return shader;
}

int main(int argc, char* argv[])
{
    // 初始化Window窗口
    sf::ContextSettings settings;
    settings.depthBits = 24;
    settings.stencilBits = 8;

    const unsigned int WIDTH = 800;
    const unsigned int HEIGHT = 600;
    const sf::String TITLE = "Modern OpenGL";
    sf::Window window(sf::VideoMode(WIDTH, HEIGHT, 32), TITLE, 
        sf::Style::Titlebar | sf::Style::Close, settings);

    // 初始化GLEW
    glewExperimental = GL_TRUE;
    glewInit();

    // 顶点数据
    GLfloat vertices[] = {
        0.0f, 0.5f, 1.0f, 0.0f, 0.0f,
        0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
        -0.5f, -0.5f, 0.0f, 0.0f, 1.0f
    };

    // 创建Vertex Array Object
    GLuint VAO;
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);

    // 创建Vertex Buffer Object并装载数据
    GLuint VBO;
    glGenBuffers(1, &VBO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 创建Vertex渲染器和Fragment渲染器
    GLuint vertexShader = createShader(GL_VERTEX_SHADER, vertexSource);
    GLuint fragmentShader = createShader(GL_FRAGMENT_SHADER, fragmentSource);

    // 将Vertex渲染器和Fragment渲染器关联到Shader Program
    GLuint shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);
    glUseProgram(shaderProgram);

    // 设置Vertex数据的布局属性(这里包括postion和color两个属性)
    GLint posAttrib = glGetAttribLocation(shaderProgram, "position");
    glEnableVertexAttribArray(posAttrib);
    glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), 0);

    GLint colorAttrib = glGetAttribLocation(shaderProgram, "color");
    glEnableVertexAttribArray(colorAttrib);
    glVertexAttribPointer(colorAttrib, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(GLfloat), (void*)(2 * sizeof(GLfloat)));

    // 这个While循环是SFML的固定模式用于做事件处理
    while (window.isOpen())
    {
        // 内层While循环用于处理事件响应
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
                window.close();
        }
        window.setActive();
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        // 绘制图形
        glDrawArrays(GL_TRIANGLES, 0, 3);
        window.display();
    }
    // 释放资源
    glDeleteProgram(shaderProgram);
    glDeleteShader(fragmentShader);
    glDeleteShader(vertexShader);

    glDeleteBuffers(1, &VBO);
    glDeleteVertexArrays(1, &VAO);

    return EXIT_SUCCESS;
}

下面大概说明一下上面的程序,具体的函数说明,等有时间了再后续的博客中再一一道来。 1-2行声明了包含了头文件,一个为glew头文件,一个为SFML的Window头文件 4行是一个宏定义,用于将GLSL的源文件和前面的版本声明信息链接起来。 7-15行是一个以字符串表示的GLSL源程序,是一个Vertex Shader。用于接收输入的顶点位置和颜色信息,并输出颜色信息传递给下一个渲染阶段。 18-24行也是一个以字符串表示的GLSL源程序,是一个Fragment Shader。接收颜色信息的输入,并输出用于Fragment渲染。 对于Vertex Shader和Fragment Shader维基百科中的解释如下:

A Vertex Shader in OpenGL is a piece of C like code written to the GLSL specification which influences the attributes of a vertex. Vertex shaders can be used to modify properties of the vertex such as position, color, and texture coordinates.

A Fragment Shader is similar to a Vertex Shader, but is used for calculating individual fragment colors. This is where lighting and bump-mapping effects are performed.

27-33行定义了一个用于创建Shader的函数。 38-46使用SFML库定义了显示图形的窗口。 49-50初始化GLEW。 53-69定义顶点数据,创建VAO和VBO对象,并在VBO中装载数据。对于VAO和VBO,维基百科给出了这样的解释:

A Vertex Array Object (VAO) is an object which contains one or more Vertex Buffer Objects and is designed to store the information for a complete rendered object. In our example this is a diamond consisting of four vertices as well as a color for each vertex.

A Vertex Buffer Object (VBO) is a memory buffer in the high speed memory of your video card designed to hold information about vertices. In our example we have two VBOs, one that describes the coordinates of our vertices and another that describes the color associated with each vertex. VBOs can also store information such as normals, texcoords, indicies, etc.

71-79行创建Vertex渲染器和Fragment渲染器并将其关联到Shader Program。 82-88行设置Vertex数据的布局属性(这里包括postion和color两个属性),将顶点数据传递给GLSL程序。 91-106行用于用户窗口事件处理,同时在While循环里面绘制图形。 108-113是最后资源的释放。

最后的运行结果如下:

最后推荐我觉得写得很好的两个在线教程: https://open.gl/ http://learnopengl.com/

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券